HTML 5 Shoot 'em Up in an Afternoon
Learn (or teach) the basics of Game Programming with this free
Phaser tutorial
Bryan Bibat
This book is for sale at http://leanpub.com/html5shootemupinanafternoon
This version was published on 2015-07-13
Leanpub
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process.
Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and many
iterations to get reader feedback, pivot until you have the right book and build traction once you do.
©2014 - 2015 Bryan Bibat
Contents
Preface i
License i
Introduction 1
Who is this book for? 1
Morning: Preparing for the Afternoon 2
Introduce them to Shoot ‘em Ups 2
Technical Requirements: JavaScript and Math 2
Development Environment Setup 3
Other Suggested Prior Reading 3
Video Walkthrough 3
Afternoon 0: Overview of the Starting Code 4
Afternoon 1: Sprites, the Game Loop, and Basic Physics 7
Sprite Basics 7
The Game Loop 13
Apply Physics 15
Afternoon 2: Player Actions 20
Keyboard Movement 20
Mouse/Touch Movement 22
Firing Bullets 24
Afternoon 3: Object Groups 27
Convert Bullets to Sprite Group 27
Enemy Sprite Group 29
Player Death 30
Convert Explosions to Sprite Group 32
Intermission: Refactoring 35
Refactoring Functions 35
Reducing Hard-coded Values 39
Afternoon 4: Health, Score, and Win/Lose Conditions 43
Enemy Health 43
Player Score 45
Player Lives 46
Win/Lose Conditions, Go back to Menu 48
Afternoon 5: Expanding the Game
53
CONTENTS
Harder Enemy 53
Power-up 60
Boss Battle 65
Sound Effects 70
Afternoon 6: Wrapping Up 73
Restore original game flow 73
Sharing your game 74
Evening: What Next? 77
Challenges 77
What we didn’t cover 80
Appendix A: Environment Setup Tutorials 81
Basic Setup 81
Advanced Setup 84
Cloud IDE Setup 85
Appendix B: Expected Code Per Chapter 90
Preface
I’ll be honest and get this out as early as possible: I’m not a “professional” game developer. Looking at
my other Leanpub books will tell you that I’m more into web development. Heck, if you told me a few
months ago that I would be putting out a game development book, I would’ve thought you’re crazy.
This book was a result of three things that happened to occur around the same time:
First was the problem that came up with our HTML5 workshop. The original lecturer bailed at the last
minute and we had problems with finding a replacement. We even considered the worst case, cutting out
the hands-on portion leaving us with a morning “workshop” consisting only of talks from people in the
local gaming industry.
Coincidentally, I was playing around with Phaser a few weeks before the event. While I am not a game
developer, I had just enough knowledge to make a simple workshop to introduce basic game concepts via
the said HTML5 game framework. In the end I volunteered to take over the workshop less than four days
before the actual event.
Normally I would have prepared a hundred or so slides and go through them during the workshop. But
earlier that week I had the rare opportunity to talk to the first person who gave me advice when I started
out teaching, and one of the things we talked about the not-so-recent trend of lazy college professors
making only slides leaving a big gap between them and textbooks. This convinced me to switch things
up with the workshop - instead of giving the participants a link to SpeakerDeck, I would point them to
Leanpub.
It took a few sleepless nights to write the original 36-page workbook, but it was worth it: I had a much
easier time conducting the workshop than I would have if I went with slides.
The positive response from the participants also convinced me to spend some more time to improve this
book and get it out there for anyone interested in learning the basics of game development.
License
This work is licensed under the Creative Commons Attribution-NonCommercial- ShareAlike 3.0 Unported
License. To view a copy of this license, visit http://creativecommons.Org/licenses/by-nc-sa/3.0/ or send a
letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.
You can find the complete source of this book at https://github.com/bryanbibat/html5shootemupinanafternoon.
Phaser © 2013-2015 Photon Storm Ltd.
Art assets derived from SpriteLib, © 2002 Ari Feldman.
Sound assets © 2012 - 2013 dklon (Devin Watson).
Introduction
This is usually the part where books give a lengthy intro about HTML5 to increase their word count. This
is not one of those books.
All you need to know about HTML5 is that it allows you to do stuff in your browser, regardless if it’s on
a desktop PC or a mobile phone, without the need for extra plugins. And that includes making games. If
you want a better intro to HTML5, head over to Dive Into HTML5.
As the title and cover of the book implies, we will introduce you to both HTML5 and game development
by guiding you in making a shoot-em-up game similar to the classic video game 1942.
There are a number of HTML5 libraries and frameworks out there right now. For this afternoon workshop,
we’ll be using Phaser, an open-source framework built on top of Pixi.js. It’s a higher-level framework: it’s
bigger and may feel like you have much less control (i.e. magical ) compared to other frameworks, but at
the same time, you need far less code to get things done and this makes it suitable for a short workshop
such as this one.
Who is this book for?
This book is for people who want to learn the basic concepts behind creating games. As a workshop
manual, it is also for experienced developers interested in introducing those concepts to those people.
With these in mind, here are some possible setups for using HTML5 Shoot ‘em Up in and Afternoon :
• Self-study - AKA your run-of-the-mill tutorial where you just go through the book from cover-
to-cover. Web developers with extensive experience in JavaScript will find the code in this book
easy and fairly straightforward. Novice programmers might not get the same pleasant experience,
especially those who have not yet coded in JS enough to understand its quirks.
• Pair or Small-group Study - Spend an afternoon teaching game programming to your daughter /
cousin / nephew. It’s recommended to go through the book once or twice beforehand to make sure
things go smoothly. (Unless of course you want to expose the kid to the reality of “spend minutes
or hours looking for the copy-paste typo” software development.)
• Workshop - What this book was originally written for. Gather a group of people interested in
making games in HTML5 and go through the tutorial at a slower pace. An experienced instructor
(i.e. worked with Phaser for some time, gone through the tutorial multiple times) can lead a
workshop of 20 without a hitch, but for larger groups or groups with less programming experience,
you may need to get a few extra mentors to help.
Morning: Preparing for the Afternoon
For instructors tutoring children or other individuals with little programming or even gaming experience,
we recommended spending a few hours in the morning to make sure things go smoothly in the afternoon.
If you’re the student, it’s best you skip this part so as not to spoil what your teacher is going to ask you to
do.
Introduce them to Shoot 'em Ups
It may sound weird for us who grew up in the ’80s and ’90s where shoot ‘em ups were staple arcade games,
but there is a very slight possibility that the person you’re teaching may not be familiar with the genre.
If that’s the case, then you need to let them play a few shoot ‘em ups before starting the workshop. They
must first understand the basic concepts around the genre, knowing what makes those games fun an
challenging. At the worst case, finding out that they hate the genre will let you end the session early and
spare you from an unproductive afternoon.
An obvious choice would be 1942, as it has been ported and remade so many times that you can find one
on pretty much any platform.
Then there are Flash games from sites like Newgrounds and Kongregate. As F1TML5 is supposed to replace
Flash, letting your student play these games will give them an idea on what they can make in the future.
Steam also has a good collection of shmups. Jamestown deserves special mention because it lets you play
with your students via local co-op.
Technical Requirements: JavaScript and Math
Theoretically, you can conduct a workshop with students who have no prior knowledge of JavaScript.
They will be at the mercy of the copy-paste gods however, and it’s also safe to say that they won’t retain
much after this workshop is over.
For best results, students who aren’t familiar with programming or JavaScript must take a crash course in
the morning. You don’t need to go all the way into advanced JavaScript - knowing how to make and use
functions and objects as well as using browser’s developer consoles for debugging should be enough for
the workshop. MDN has a good list of JavaScript tutorials that you and your students can choose from
for this purpose.
In addition to programming skills, students should know basic Trigonometry. Phaser already handles most
of the calculation but knowing stuff like sine/cosine and polar coordinates will make it easier for them to
visualize what’s going on under the hood. They will also directly use those concepts at the latter part of
the workshop where we rotate sprites and generate patterns for the boss battle.
While there are many online tutorials out there for trigonometry (Khan Academy comes to mind), I have
yet to see one that is better than your usual high school trigonometry class while accessible to younger
students. You might even say that the other way around, introducing kids to trigonometry through game
concepts, would be a better approach 1 .
J True story: I discovered sine and cosine as way to make things spin or bob up and down back when I was a kid playing around with BASIC,
two years before I had trigonometry class.
Morning: Preparing for the Afternoon
3
If you still wish to quickly introduce basic trigonometry to your students before the workshop, look for
visually impressive and interactive demos like How to Fold a Julia Fractal.
Development Environment Setup
All you need to code in Phaser is a browser that supports HTML5 (e.g. Chrome, Firefox), a web server,
and the text editor of your choice.
You have to use a web server to test your game in this tutorial. The first part of Getting Started With
Phaser explains why you should do this.
As for the text editor, any editor or IDE with JavaScript support (syntax highlighting, automatic
indent/brackets) and can parse non-Windows line endings (i.e. not Notepad) will do. If your preferred
editor does not fit these requirements, we suggest downloading the free trial of Sublime Text.
Once you have setup your web server and text editor, download the basic game template with Phaser 2.4
RCl from Github, extract it to the folder served by the web server, and start coding.
More detailed information about setting up your development environment (like choosing a web server)
can be found at Appendix A: Environment Setup Tutorials.
Other Suggested Prior Reading
Apart from JS and Math, we suggest that you at least skim through the following to give you an idea
about what we are going to do:
• Getting Started with Phaser - Phaser’s own guide setting up a development environment
• Phaser Examples - view demos of Phaser’s features.
• Phaser Documentation - your typical API docs with link to source.
In addition to Phasers documentation, the following may give you insights on making games in Phaser:
• Game Programming Patterns - like many game frameworks, Phaser uses the Game Loop pattern
at its core.
• Game Mechanic Explorer - a somewhat short list of game mechanics, all implemented in Phaser.
Video Walkthrough
This book reached the Leanpub’s “Lifetime Number of Copies Sold” bestsellers list around December 2014.
As my holiday gift of thanks to those that bought and downloaded it, I recorded a quick and dirty video
walkthrough of the main chapters of the book. If you prefer watching the programming lessons in HD
video (even when they are taught by a slightly drunk non-native English speaking guy), you’re in luck.
If you purchased or downloaded the book, you should be able to download the videos (all 600MB+ of
them) via the “Extras” zip link on your Leanpub dashboard. If you’re reading this online or want to watch
in lower resolution, you can also watch the videos on YouTube.
Afternoon 0: Overview of the Starting Code
By now you should have finished setting up your development environment, with your web server up
and your editor open to the folder containing the base code for the tutorial. If not, please refer again to
the Development Environment Setup in previous chapter.
Before we proceed to the actual tutorial, let’s take a tour of the starter template:
This template is based on the Basic Game Template found in the resources/Project Templates folder
of the Phaser Git repository. We’re using this because it follows a more modular approach compared to
most of the Phaser Examples and therefore much closer to real-life apps.
Let’s do a quick run-through of the files:
• index . html - our main HTML5 page that links all our files together. There’s not much to say about
this except for the <div id="gameContainer"x/div> which Phaser will use to draw the Canvas
on to.
• phaser - arcade -physics . min . js - Phaser stripped of 2 other physics engines (retaining only
“Arcade” physics) and minified. You can replace this later on with the full version if you plan
to use the other physics engines or if you want to the refer to the original code while developing.
• app . js - the code that kicks off the app. Creates the Phaser . Game object and adds the States.
• boot . js, preloader . js, mainMenu . js, game . js - the different states of our game, combined together
by app . js to form the flow of our app:
- Boot - The initial state. Sets up additional settings for the game. Also pre-loads the image for
the pre-loader progress bar before passing the game to Preloader.
- Preloader - Loads all assets before the actual game. Once that’s done, the game proceeds to
MainMenu.
- MainMenu - The title screen and main menu before the actual game.
- Game - The actual game.
Reading through the JS files and the comments within will give you a peek of what to expect from Phaser.
Afternoon 0: Overview of the Starting Code
5
File Edit View
Go Bookmarks Help
♦ * *
o *
m i[m)
Downloads
/ htmlSshmup-template ^ assets
.J !■
* 0 s “
OB
bomb. png
bomb-blast.
png
+++ ■
boss.png
o
bullet. png
o
bullet-burst.
png
1
destroyer.png
444- t
enemy.png
o
enemy-bullet.
png
IfiJ
enemy-fire.
ogg
a
enemy-fire.
wav
1 1 J
explosion. ogg
explosion, png
explosion.wav
yjr»y|
player.png
m
player-
explosion. ogg
player-
explosion, wav
l£J
player-fire.ogg
m
player-fire.wav
l£J
powerup.ogg
m
powerup.wav
*
powerupl.png
♦
powerup2.png
preloader-bar.
png
■
sea. png
444-, 1
shooting-
enemy.png
Nllll
sub. png
titlepage.png
[0 27 items, Free space: 920.6 MB
The template also includes all the necessary sprites and sounds for the basic game, saving you hours of
looking for or making your own game assets. The sprites were taken from Ari Feldman’s open-sourced
sprite compilation SpriteLib while the sounds were from Devin Watson’s OpenGameArt.org portfolio.
With the code tour out of the way, we can now move on to the tutorial.
Code Examples
You will see sample code throughout this manual. The text decoration in the code will tell you what
you need to do to the existing code.
For example, let’s modify game . js to make the background scroll vertically:
update: function () {
— // — Honestly, — just about anything could go here. — It's YOUR gome after oil. . .
this . sea . ti lePosition . y += 0.2;
},
In the code example above, there is a strikethrough on the comment. Strikethrough means you need to
delete those lines. On the other hand, the following line is in boldface, which means you need to insert
those lines at that position.
Some examples for inserting functions will also have line numbers to tell you where to insert those
functions. They will also give you an idea if you’ve properly added the code up to that point.
Skipping Main Menu
We’ll be modifying our code many times throughout this tutorial. Skipping the boot, pre-loading, and
main menu in order to go directly to our game, will save us a click after the refresh every time we make
a change. To skip those states, change the starting state in app . js:
— game . state . start ( ' Boot ' ) ;
game . state . start( ' Game ' ) ;
And since we’re skipping the pre loader . js, we’ll copy over the sea background asset loading to
game . js:
Afternoon 0: Overview of the Starting Code
6
BasicGame . Game . prototype = {
preload: function () {
this . load . image( ' sea ' , ' assets/sea . png ' ) ;
},
create: function () {
WebGL lag workaround
Phaser automatically detects if your browser supports WebGL and will use it if possible.
While it usually translates to faster performance on devices with graphics processors, WebGL rendering
can be slow and laggy on other machines. If you’re noticing significant lag on your browser, you can
force Phaser to use plain HTML Canvas by changing the following line in app . js :
— var game = now Phaser . Gamo( 800, — 669, — Phaser . AUTO, — ' gameContainer ' ) ;
var game = new Phaser . Game(800, 600, Phaser. CANVAS, 'gameContainer');
Afternoon 1: Sprites, the Game Loop, and
Basic Physics
In this first part, we’ll go over how to draw and move objects on our game.
Sprite Basics
Draw Bullet Sprite
Let’s start with something basic - drawing an object on the game stage. The most basic object in Phaser
is the Sprite. So for our first piece of code, let’s load then draw a bullet sprite on our game by making
the following modifications to game . js. (All of the code examples in this tutorial refer to game . js unless
otherwise noted.)
preload: function () {
this . load . image( ' sea ' , ' assets/sea . png ' ) ;
this. load. image ( 'bullet' , ' assets/bul let . png ' );
},
create: function () {
this .sea — this . add . ti leSprite(0, 0, 800, 600, sea );
this. bullet = this . add . sprite(400, 300, 'bullet');
}
We called the following functions:
• load. image( ) - loads an image (e.g. assets/bul let . png) and assigns it a name (e.g. bullet) which
we use later.
• add.sprite() - accepts the x-y coordinates of our sprite and the name of the sprite which we
assigned in the load . image( ) function.
Afternoon 1: Sprites, the Game Loop, and Basic Physics
8
Bullet sprite added into our game
Screen Coordinates vs Cartesian Coordinates
At around middle school, children learn about the Cartesian coordinate system where points, defined by
an ordered pair (x, y), can be plotted on a plane. The center is (0, 0), x- values increase as you go right,
while y-values increase as you go up.
( 0 , 0 )
5 -
10 -
15
20
25
30 -
10 15 20 25 30 35 40
i x-axis
4
[ 15 , 10 )
Y y-axis
Cartesian coordinate system
screen coordinate system
However, computer displays do not use Cartesian coordinates as is but instead use a variation: instead of
being at the center, (0, 0) represents the point at the top-left, and instead of decreasing, y-values increase
as you go down. This picture illustrates the screen coordinate system in our game at the moment:
Afternoon 1: Sprites, the Game Loop, and Basic Physics
9
Afternoon 1: Sprites, the Game Loop, and Basic Physics
10
A note about the Phaser Examples
The biggest difference between the Phaser Examples and our game template is that the former
uses global variables while we’re adding States which encapsulate the logic of our game. This
means that you can’t copy the code from those examples directly. For example, 01 - load an
image uses the following syntax:
game . add . sprite(0, 0, 'einstein');
We don’t have a game global variable within the scope in our BasicGame.Game state object.
Instead, we have a this . game property so translating the code above into our template would
be:
this . game . add . sprite(0, 0, 'einstein');
Adding this. game over and over in your code might make you wish for a global variable.
Fortunately, Phaser also adds the other game properties into the state. You can see a list of this
in the original game template:
BasicGame.Game = function (game) {
// When a State is added to Phaser it automatically has the following
// properties set on it, even if they already exist:
this .
. game;
//
this .
. add;
//
this .
.camera;
//
this .
. cache;
//
this
. input;
//
//
this
. load;
//
this
.math;
//
this
. sound;
//
this .
. stage;
//
this .
.time;
//
this .
. tweens;
//
this .
. state;
//
this .
.world;
//
this .
. particles;
//
this
. physics ;
//
this
.rnd;
//
a reference to the currently running game
used to add spri tes, text, groups, etc
a reference to the game camera
the game cache
the global input manager (you can access this . input . keyboard,
this . input . mouse, as well from it)
for preloading assets
lots of useful common math operations
the sound manager - add a sound, ploy one, set-up markers, etc
the game stage
the clock
the tween manager
the state manager
the game world
the particle manager
the physics manager
the repeatable random number generator
// You can use any of these from any function within this State.
// But do consider them as being 'reserved words', i.e. don't create a property
// for your own game called "world" or you'll over -write the world reference .
};
In other words, you only need to use th i s . add in place of th i s . game . add :
this . add . sprite(0, 0, 'einstein');
Which is exactly what we used for adding a sprite above.
Afternoon 1: Sprites, the Game Loop, and Basic Physics
11
Draw Enemy Animation
Let’s then proceed with something more complicated, an animated sprite.
We first load a sprite sheet, an image containing multiple frames, in the pre-loading function.
preload: function () {
this . load . image( ' sea ' , ' assets/sea . png ' ) ;
this . load . image( ' bul let 1 , ' assets/bul let . png ' ) ;
this . load . spritesheet( ' greenEnemy ' , ' assets/enemy . png ' , 32, 32);
},
Instead of load . image( ), we used load . spr itesheet( ) to load our sprite sheet. The two additional
arguments are the width and height of the individual frames. Since we defined 32 for both width and
height, Phaser will load the sprite sheet and divide it into individual frames like so:
| 32px 1
0 12 3
Enemy sprite sheet (magenta refers to the transparent parts of the image)
Now that the sprite sheet is loaded, we can now add it into our game:
create: functionQ {
this .sea - this . add . ti leSprite(0, 0, 800, 600, 'sea');
this. enemy = this . add . sprite(400, 200, 'greenEnemy');
this . enemy . animations . add( ' fly ' , [ 0, 1, 2 ], 20, true);
this . enemy .pi ay ('fly');
this. bullet = this . add . sprite(400, 300, 'bullet');
}
The animations . add( ) function specified the animation: its name, followed by the sequence of frames in
an array, followed by the speed of the animation (in frames per second), and a flag telling whether the
animation loops or not. So in this piece of code, we defined the fly animation that loops the first 3 frames
of the green enemy sprite sheet, an animation of the propeller spinning:
Afternoon 1: Sprites, the Game Loop, and Basic Physics
12
Ordering
Note how we added the bullet sprite after the enemy sprite. As we shall see later, this declaration
will put the bullet sprite above the enemy sprite.
There are ways to rearrange the order of the sprites (e.g. top to bottom) but the simplest way
is to create them already in a bottom-to-top order.
Set Object Anchor
The sprites share the same x-coordinate, so by default they are left-aligned.
For games, however, most of the time we want the x-y coordinates to be the center of the sprite. We can
do that in Phaser by modifying the anchor settings:
Afternoon 1: Sprites, the Game Loop, and Basic Physics
13
this. enemy = this . add . sprite(400, 300, ' greenEnemy ' ) ;
this . enemy . animations . add( ' fly ' , [0, 1, 2 ], 20, true);
this . enemy ,play( 1 fly' );
this. enemy .anchor. setTo( 0.5, 0.5);
this. bullet = this . add . sprite(400, 400, 'bullet');
th i s. bu 1 let. anchor. setTo (0.5, 0.5);
The (0.5, 0.5) centers the sprite. On the other hand, (0, 0) will mean that the x-y coordinate defines
the top-left of the sprite. Similarly, (1 , 1 ) put the x-y at the bottom right of the sprite.
The Game Loop
The following is an oversimplified diagram on what happens when Phaser games run:
state. start( )
* >
Preload
. .
after all assets
are loaded
Create
after a set
amount of time
r
Update
Render
Game Loop
• Preload - The game starts with a pre-load section where all assets are pre-loaded. Without pre-
loading, the game will stutter or hang in the middle of gameplay because it has to load assets.
Afternoon 1: Sprites, the Game Loop, and Basic Physics
14
• Create - After pre-loading all assets, we can now setup the initial state of the game.
• Update - At a set interval (usually 60 times per second), this function is called to update the game
state. All updates to the game are done here. For example, checking if the character has collided
with the enemy, spawning an enemy at a random location, moving a character to the left because
the player pressed the left arrow key, etc.
• Render - coming after Update, here is where the latest state of the game is drawn (rendered) to the
screen.
The update-render loop is what’s called the Game Loop, and is the heart of almost every computer game.
You can read more about the Game Loop at the Game Programming Patterns site.
Move Bullet via update()
Now that we know how the game loop is implemented in Phaser, let’s move our bullet sprite vertically
by reducing its y-coordinate in the update( ) function:
update: function () {
this . sea . ti lePosition . y += 0.2;
this. bullet. y -= 1;
},
As mentioned above, Phaser will call the update( ) function at a regular interval, effectively moving the
bullet upwards at a rate of around 60 pixels per second.
...
*
« i#r>
- |update()] -
render( )
-|update( )|— | render( ) j -|update() j— |render()j
-|render( )j—
This is how you move sprites in most basic game libraries/frameworks. In Phaser, though, we can let the
physics engine do almost all of the dirty work for us.
Missing renderQ?
Before we discuss how to use Phaser’s physics engines, let’s explain why we still don’t have a render ( )
function and yet the game renders the game state on its own.
First off, as some might have noticed from the app . js, we’re only coding a portion of the game called
the state which, as the name implies, is a state of the game.
game . state . add( ’ Boot ' , BasicGame . Boot) ;
game . state . add ( 'Preloader' , BasicGame . Preloader ) ;
game . state . add( ’ MainMenu ' , BasicGame . Main Menu) ;
game . state . add( ' Game 1 , BasicGame . Game) ;
Afternoon 1: Sprites, the Game Loop, and Basic Physics
15
The state is just one of the many things updated and rendered in Phaser’s game loop. For instance, here’s
what the Game object calls on update (pre- and post-update hooks removed):
this . state . update( ) ;
this . stage . update( ) ;
this . tweens . update( ) ;
this . sound . update( ) ;
this . input . update ( ) ;
this . physics . update( ) ;
this . particles . update( ) ;
this . plugins . update( ) ;
And here’s the render section:
this . Tenderer . render (this . stage) ;
this . plugins . render ( ) ;
this . state . render( ) ;
this . plugins . postRender( ) ;
We don’t need to write code to render our sprites because that is already covered by the first line,
this . Tenderer . render(this . stage) ; , with this. stage containing all of the sprites currently in the
game.
We’ll write some render code later for debugging purposes.
Apply Physics
Phaser comes with 2 physics systems, Arcade and P2. Arcade is the default and the simplest, and so we’ll
use that.
(And besides, the version of Phaser bundled with the basic template, phaser-arcarde-physics . min . js,
contains only Arcade physics to reduce download file size.)
Velocity
Once we put our bullet into the Arcade physics system, we can now set its velocity and let the system
handle all the other calculations (e.g. future position).
this. but let = this . add . sprite(400, 400, 'bullet');
this . bul let . anchor . setTo(0 . 5, 0.5);
this . physics . enable(this .bullet, Phaser. Physics .ARCADE) ;
this. bullet. body. velocity .y = -500;
update: function () {
this . sea . ti lePosition . y += 0.2;
— this . bul let . y — ~ 1 ;
},
Afternoon 1: Sprites, the Game Loop, and Basic Physics
16
With the physics enabled and velocity set, our sprite’s coordinates will now be updated by the
this . physics . update( ) ; call rather than our update code. In this case, “velocity, y = -500” is 500
pixels per second upward; at 60 frames per second, each update call will move the bullet up 8-9 pixels.
update( )
render( )
J -^updateQ^ — ~^renderoJ
update( ) I
render( )
update( ) I
render( ) I
Show Body Debug
Arcade physics is limited to axis-aligned bounding box (AABB) collision checking only. In simpler terms,
all objects under Arcade are rectangles.
bounding boxes (hitboxes) of the sprites outlined in red; the sprites to the right are colliding with each other
We can view these rectangles by rendering these areas with the debugger. First we add the enemy sprite
to the physics system:
this . enemy .play( ' fly' );
this . enemy . anchor . setTo(0 . 5, 0.5);
this. physics ,enable(this .enemy, Phaser. Physics .ARCADE) ;
this. bullet = this . add . sprite(400, 300, 'bullet');
Then we add the debugging code under our currently nonexistent render ( ) function:
30
31
32
33
34
35
36
37
42
43
44
45
Afternoon 1: Sprites, the Game Loop, and Basic Physics
17
update: function () {
this . sea . ti lePosition . y += 0.2;
},
render functionQ {
this . game . debug . body ( this . bullet);
this . game . debug . body(this . enemy) ;
},
Collision
Once added to the physics system, checking collision and overlapping is only a matter of calling the right
functions:
update: function () {
this . sea . ti lePosition . y += 0.2;
this . physics . arcade . overlap(
this. bullet, this. enemy, this . enemyHit, null, this
);
},
The overlap( ) function requires a callback which will be called in case the objects overlap. Here’s the
enemyHit() function:
enemyHit: function (bullet, enemy) {
bullet.killQ;
enemy . ki 1 1 ( ) ;
},
Being common situation in games, Phaser provides us with a sprite . ki 1 1 ( ) function for “killing” sprites.
Calling this function both marks the sprite as dead and invisible, effectively removing the sprite from the
game.
Here’s the collision in action:
With debug on, we can see that the sprite is still at that location but it’s invisible and the physics engine
ignores it (i.e. it no longer moves).
Afternoon 1: Sprites, the Game Loop, and Basic Physics
18
Remove Debugging
Debugging isn’t really required in this workshop so you should probably remove or comment out the
debugging code when you’re done testing.
render: function() {
this . gam e . d e bug . body(this . bull e t);
this . game . debug . body (this . enemy) ;
},
Explosion
Before we proceed to the next lesson, let’s improve our collision handling by adding an explosion
animation in the place of the enemy. Here’s the animation pre-loading:
preload: function () {
this . load . image( ' sea ' , ' assets/sea . png ' ) ;
this . load . image( ' bul let ' , ' assets/bul let . png ' ) ;
this . load . spritesheet( ' greenEnemy ' , 1 assets/enemy . png ' , 32, 32);
this . load . spritesheet( ' explosion ' , ' assets/explosion . png ' , 32, 32);
},
mw
m
] 0
V-
Then the actual explosion:
enemyHit: function (bullet, enemy) {
bullet.killQ;
enemy . ki 1 1 ( ) ;
var explosion = this. add. sprite(enemy.x, enemy. y, 'explosion');
explosion. anchor. setTo(0. 5, 0.5) ;
explosion . animations . add( ' boom ' ) ;
explosion . play (' boom ' , 15, false, true);
},
Here we used a different way to setup animations. This time we used an i mat ions, add () with only the
name of the animation. Lacking the other arguments, the boom animation uses all frames of the sprite
sheet, runs at 60 fps, and doesn’t loop.
We want to tweak the settings of this animation, so we add them to the explosion . play ( ) call as
additional arguments:
• 15 - set the frames per second
• false - don’t loop the animation
• true - kill the sprite at the end of the animation
Afternoon 1: Sprites, the Game Loop, and Basic Physics
19
The last argument the most convenient to us; without it we’ll need to register an event handler callback
to perform the sprite killing, and event handling is a much later lesson. In the meantime, enjoy your
improved “shooting down an enemy” animation:
< .*■ >
m
vi
•
*
Afternoon 2: Player Actions
Now that we’re done with drawing and movement, let’s move on to making an object that will represent
us in the game. Load the player sprite in the preload( ) function:
preload: function () {
this . load . image( ' sea ' , ' assets/sea . png 1 ) ;
this . load . image( ' bul let ' , ' assets/bul let . png ' ) ;
this . load . spritesheet( ' greenEnemy ' , 1 assets/enemy . png ' , 32, 32);
this . load . spritesheet( ' explosion ' , 1 assets/explosion . png 1 , 32, 32);
this . load . spritesheet( ' player ' , 'assets/player. png ' , 64, 64);
},
Add the following to the create ( ) function before the enemy sprite to add our sprite into the game:
this .sea - this . add . ti leSprite(0, 0, 800, 600, 'sea');
this. player = this . add . sprite(400, 550, 'player');
th i s. p 1 ayer. anchor. setTo (0.5, 0.5);
this . player . animations . add( ' fly ' , [ 0, 1, 2 ], 20, true);
this . player .pi ay ('fly');
this . physics ,enable(this .player, Phaser. Physics .ARCADE) ;
this. enemy = this . add . sprite(400, 200, 'greenEnemy');
Keyboard Movement
Implementing keyboard-based input is straightforward in Phaser. Here we begin by using a convenience
function which returns the four arrow keys.
this . bul let . anchor . setTo(0 . 5, 0.5);
this . enable(this .bullet, Phaser . Physics .ARCADE) ;
this . bul let . body . velocity . y = -500;
this. cursors = this. input. keyboard. createCursorKeys();
},
Let’s also set the player’s initial speed speed as a property of the player object on create since we’ll be
using this value multiple times throughout our program:
Afternoon 2: Player Actions
21
this. player = this . add . sprite(400, 550, 'player');
this . player . anchor . setTo(0 . 5, 0.5);
this . player . animations . add( ' fly ' , [0, 1, 2 ], 20, true);
this . player ,play( ' fly' );
this . physics . enable(this . player, Phaser . Physics . ARCADE) ;
this . player . speed = 300;
this. enemy = this . add . sprite(400, 200, ' greenEnemy ' ) ;
This will also allow us to have planes with different speeds or “speed up” type of power-ups later.
Once that’s done, we can now set the velocity like so:
update: function () {
this . sea . ti lePosition . y += 0.2;
this . physics . arcade . overlap(
this. bullet, this. enemy, this.enemyHit, null, this
);
this . player . body . velocity .x = 0;
this . player . body . velocity . y = 0;
if (this. cursors. left. isDown) {
this . player . body . velocity . x = -this. player. speed;
} else if (this. cursors. right. isDown) {
this . player . body . velocity . x = this. player. speed;
}
if (this. cursors. up. isDown) {
this . player . body . velocity . y = -this. player. speed;
} else if (this. cursors. down. isDown) {
this . player . body . velocity . y = this. player. speed;
}
Note that we set the velocity to zero so that the plane stops when the input stops. We also allow the player
to input both vertical and horizontal movement at the same time.
Afternoon 2: Player Actions
22
Arcade physics also makes it easy to make the edges of the stage act like walls:
this . physics . enable(this . player, Phaser . Physics . ARCADE) ;
this.pl ayer .speed = 300 ;
this. player. body. collideWorldBounds = true;
this. enemy = this . add . sprite(400, 300, ' greenEnemy ' ) ;
Mouse/Touch Movement
Point-based movement usually requires hand-rolling your mathematical calculations. Fortunately, Phaser
already has functions which calculates the angle and velocity based on input points.
Here’s how simple it is to move an object towards the pointer:
this . player . body . velocity . y = this . player . speed;
}
if (this. input. activePointer. isDown) {
this . physics . arcade . moveToPointer(this . player, this . player . speed) ;
}
Afternoon 2: Player Actions
23
Based on the object’s location and a speed, the Arcade physics function moveToPointer( ) calculates the
angle and velocities required to move towards the pointer at the input speed. Calling this function will
already modify the x and y velocities of the object, which is exactly what we need in this situation.
This function will not rotate the sprite, though, so if you need to rotate the sprite accordingly, you can
use the return value of the function which is the angle of rotation in radians. We shall see an example of
this in a later lesson.
Just a word of warning, the movement in a frame may overshoot the target (i.e. move 5 pixels even though
the pointer is 2 pixels away) causing your player sprite to tremble instead of staying put. The inaccurate
coordinates given by a touch screen may also produce a similar effect. A crude way of getting over these
is to stop movement at a certain distance from the pressed point, like so:
this . player . body . velocity . y = this . player . speed;
}
— if (this, input . activePointor . isDown) — {-
if (this. input. activePointer. isDown &&
this . physics . arcade . distanceToPointer(this . player) > 15) {
this . physics . arcade . moveToPo inter (this . player, this . player . speed) ;
}
},
If you need more precise input, you may be better off implementing an on-screen directional pad.
Afternoon 2: Player Actions
24
Firing Bullets
Let’s remove our old bullet code and add new code for creating bullets on the fly.
create: function () {
this. bull e t = this . add . sprit c ( 4 00, — 300, — 'bull e t' ) ;
this . bul let . anchor . sctTo(0 . 5, — 0.5);
this . physics . c nabl c (this .bull e t, — Phas e r . Physics . ARCADE) ;
this . bul let . body . velocity . y = — 500 ;
this . bullets = [] ;
We set our fire button to Z or tapping/ clicking the screen:
update: function () {
this . physics . arcade . moveToPo inter (this . player, this . player . speed) ;
}
if (this. input. keyboard. isDown(Phaser . Keyboard. Z) II
this. input. activePointer. isDown) {
this . f ire( ) ;
}
},
Then we create a new function that will fire a bullet just above the nose of player’s sprite:
82 fire: function() {
var bullet = this . add . sprite(this . player . x, this . player . y - 20, 'bullet');
84 bul let . anchor . setTo(0 . 5, 0.5);
85 this. physics. enable(bul let, Phaser . Physics .ARCADE) ;
86 bul let . body . velocity . y = -500;
87 this . bul lets . push(bul let) ;
88 },
And finally we modify our collision detection code to iterate over the bullets:
update: function () {
this . sea . ti lePosition . y += 0.2;
this . physics . arcado . ovor lap(
this . bul l e t, — this. e n e my, this . c n c myHit, — null, — this
It
for (var i = 0; i < this. bullets. length; i++) {
this . physics . arcade . overlap (
this . bullets [i] , this. enemy, this .enemyHit, null, this
);
}
Afternoon 2: Player Actions
25
Fire Rate
One obvious problem that you’ll see as you test this new firing code is that the bullets come out at a very
high rate. We can throttle this by storing a time value specifying the earliest time when the next bullet
can be fired.
Add the variable nextShotAt and shotDelay (set to 100 milliseconds) to the create ( ) function:
this . bul lets = [ ] ;
this . nextShotAt = 0;
this . shotDelay = 100;
Then modify the f ire( ) function to check and eventually set the nextShotAt variable:
fire: function() {
if (this . nextShotAt > this . time . now) {
return;
}
this . nextShotAt = this. time. now + this. shotDelay;
var bullet = this . add . sprite(this . player . x, this . player . y - 20, 'bullet');
bul let . anchor . setTo(0 . 5, 0.5);
this . physics . enable(bul let, Phaser . Physics .ARCADE) ;
bul let . body . velocity . y = -500;
this . bul lets . push (bul let) ;
},
fire rate now down to 100 milliseconds per shot
Afternoon 2: Player Actions
26
How To Play message
We don’t have time to code a help screen, so let’s just flash the “how to play” instructions in the first 10
seconds of every session.
Add this to the end of create ( ) to add the text:
this . instructions = this . add . text( 400, 500,
'Use Arrow Keys to Move, Press Z to Fire\n' +
'Tapping/clicking does both',
{ font '20px monospace', fill: ' *f f f ' , align: 'center' }
);
this . instructions . anchor . setTo(0 . 5, 0.5);
this . instExpire = this . time . now + 10000;
And the end of update( ) to make the text disappear after the time has elapsed:
if (this . instructions . exists && this . time . now > this . instExpire) {
this . instructions . destroy( ) ;
}
Use Arrow Keys to Move, Press Z to Fire
Tapping/clicking does both
Other problems
If you haven’t noticed it yet, the other problem with our current bullet generation approach is it’s
essentially a memory leak. In the next chapter, we’ll discuss one way of limiting the resources that our
game will use.
Afternoon 3: Object Groups
Instead of creating objects on the fly, we can create Groups where we can use and re-use sprites over and
over again.
Convert Bullets to Sprite Group
Bullets are best use case for groups in our game; they’re constantly being generated and removed from
play. Having a pool of available bullets will save our game time and memory.
Let’s begin by switching out our array with a sprite group. The comments below explain our new code.
create: function () {
this. bull e ts = — hfr
// Add an empty sprite group into our game
this . bulletPool = this . add . group( ) ;
// Enable physics to the whole sprite group
this. bulletPool .enableBody = true;
this. bulletPool . physicsBodyType = Phaser. Physics. ARCADE;
// Add 100 'bullet' sprites in the group.
// By default this uses the first frame of the sprite sheet and
// sets the initial state as non-existing (i.e. killed/dead)
this . bulletPool .createMultiple(100, 1 bullet ' ) ;
// Sets anchors of all sprites
this. bulletPool .setAll( ' anchor. x' , 0.5);
this . bulletPool .setAll( ' anchor. y ' , 0.5);
// Automatically kill the bullet sprites when they go out of bounds
this . bulletPool . setAll ( ' outOfBoundsKi 11 ' , true) ;
this. bulletPool .setAll( ' checkWorldBounds ' , true);
this . nextShotAt = 0;
Let’s move on to the f ire( ) function:
Afternoon 3: Object Groups
28
fire: function() {
if (this . nextShotAt > this . time . now) {
return;
}
if (this.bulletPool .countDeadQ === 0) {
return;
}
this . nextShotAt = this . time . now + this . shotDelay ;
— var bull e t ~ this. add. sprit e (this. play e r .x, — this . play e r . y 20 -, — ' bull e t ' ) ;
— bullet. anchor . sctTo(0 . 5 , — 0 . 5 );
— this . physics . enable (bul lot, — Phaser . Physics . ARCADE) ;
— bul lot . body . velocity . y - — 500 ;
— this . bullets . push (bul lot) ;
// Find the first dead bullet in the pool
var bullet = this.bulletPool .getFirstExists(false);
// Reset (revive) the sprite and place it in a new location
bullet. reset(this. player. x, this. player. y - 20);
bul let . body . velocity . y = -500;
},
Here we replaced creating bullets on the fly with reviving dead bullets in our pool.
Update collision detection
Switching from array to group means we need to modify our collision checking code. Good news is that
overlap( ) supports Group to Sprite collision checking.
update: function () {
this . sea . ti lePosition . y += 0.2;
for (var i = 0 ; — i — < this . bul lets . length ; — i++) — {-
this . physics . arcad e . ov c rlap(
this . bul lets [ i ] , — this . enemy, — this . cncmyHit, — null, — this
ft
f
this . physics . arcade . overlap(
this.bulletPool, this. enemy, this .enemyHit, null, this
);
There is a minor quirk when comparing “Groups to Sprites” (see if you can notice it) that is not present
in “Sprite to Groups” or “Group to Groups”. This shouldn’t be a problem since we’re only doing the latter
two after this section.
Afternoon 3: Object Groups
29
Enemy Sprite Group
Our game would be boring if we only had one enemy. Let’s make a sprite group so that we can generate
a bunch more enemies so that they can start giving us a challenge:
this. enemy = this . add . spritc( 100, — 209, — ' grccnEncmy ' ) ;
this . e n e my . anchor . s c tTo(0 . 5, 0.5);
this . enemy . animations . add( ' f ly ' , — [— Q-; — — 2 ] , — 2Q -, — true) ;
this . e n e my .play ('fly');
this . physics . enable (this . enemy, — Phaser . Physics . ARCADE) ;
this . enemyPool = this . add . group( ) ;
this . enemyPool . enableBody = true;
this . enemyPool . physicsBodyType = Phaser. Physics. ARCADE;
this . enemyPool .createMultiple(50, 'greenEnemy ' );
this . enemyPool .setAll( ' anchor. x' , 0.5);
this . enemyPool ,setAll( ' anchor. y ' , 0.5);
this . enemyPool . setAll ( ' outOfBoundsKi 1 1 ' , true) ;
this . enemyPool .setAll( ' checkWorldBounds ' , true);
// Set the animation for each sprite
this . enemyPool . forEach( function (enemy) {
enemy . animations . add( ' fly ' , [0, 1, 2 ], 20, true);
});
this . nextEnemyAt = 0;
this . enemyDelay = 1000;
And again, modifying the collision code become Group to Group:
this . physics . arcade . overlap(
this . bul IctPool , — this . enemy, — this . oncmyHit, — nul 1 , — this
this.bulletPool, this . enemyPool , this . enemyHit, null, this
);
Randomize Enemy Spawn
Many games have enemies show up at scripted positions. We don’t have time for that so we’ll just
randomize the spawning locations.
Add this to the update( ) function:
Afternoon 3: Object Groups
30
update: function () {
this . sea . ti lePosition . y += 0.2;
this . physics . arcade . overlap(
this . bul letPool , this . enemyPool , this . enemyHit, null, this
);
if (this . nextEnemyAt < this . time . now && this . enemyPool . countDead( ) > 0) {
this . nextEnemyAt = this . time . now + this . enemyDelay ;
var enemy = this. enemyPool .getFirstExists(false);
// spawn at a random location top of the screen
enemy . reset(this . rnd . integerInRange(20, 780), 0);
// also randomize the speed
enemy . body . velocity . y = this . rnd . integerInRange(30, 60);
enemy . play ( ' f ly ' ) ;
}
this . player . body . velocity . x = 0;
this . player . body . velocity . y = 0;
Like our bul letPool, we also store the next time an enemy should spawn.
H
'Vr 1 '
enemy spawn area and movement range in white
Note that we did not use Math . random ( ) to set the random enemy spawn location and speed but instead
used the built-in randomizing functions. Either way is fine, but we chose the built in random number
generator because it has some additional features that may be useful later (e.g. seeds).
Player Death
Let’s further increase the challenge by allowing our plane to blow up.
Let’s first add the collision detection code:
Afternoon 3: Object Groups
31
140
141
142
143
144
145
146
147
update: function () {
this . sea . ti lePosition . y += 0.2;
this . physics . arcade . overlap(
this . bul letPool , this . enemyPool , this . enemyHit, null, this
);
this . physics . arcade . overlap(
this. player, this . enemyPool , this . playerHit, null, this
);
if (this . nextEnemyAt < this . time . now && this . enemyPool . countDead( ) > 0) {
Then the callback:
playerHit: function (player, enemy) {
enemy . ki 1 1 ( ) ;
var explosion = this . add . sprite(player . x, player. y, 'explosion');
explosion . anchor . setTo(0 . 5, 0.5);
explosion . animations . add( 1 boom ' ) ;
explosion . play( 1 boom 1 , 15, false, true);
player . ki 1 1 ( ) ;
},
You might notice that even though the plane blows up when we crash to another plane, we can still fire
our guns. Let’s fix that by checking the alive flag:
fire: function() {
if (this . n c xtShotAt > this . tim e . now) — f-
if ( !this. player. alive II this . nextShotAt > this . time . now) {
return;
}
if (this . bul letPool . countDead( ) === 0) {
return;
}
Another possible issue is that our hitbox is too big because of our sprite. Let’s lower our hitbox accordingly:
this . physics . enable(this . player, Phaser . Physics . ARCADE) ;
this.pl ayer .speed = 300 ;
this . player . body . col 1 ideWorldBounds = true;
// 20 x 20 pixel hitbox, centered a little bit higher than the center
this . player . body . setSize(20, 20, 0, -5);
This hitbox is pretty small, but it’s still on par with other shoot em ups (some “bullet hell” type games
even have a 1 pixel hitbox). Feel free to increase this if you want a challenge.
Use the debug body function if you need to see your sprite’s actual hitbox size. Don’t forget to remove it
afterwards.
Afternoon 3: Object Groups
32
render function() {
this . game . debug . body (this . player) ;
}
smaller hitbox, but still fair gameplay-wise
Convert Explosions to Sprite Group
Our explosions are also a possible memory leak. Let’s fix that and also do a bit of refactoring in the process.
Put this on the create ( ) after all of the other sprites:
this . shotDelay = 100;
this . explosionPool = this . add . group( ) ;
this . explosionPool . enableBody = true;
this . explosionPool . physicsBodyType = Phaser. Physics. ARCADE;
this . explosionPool . createMultiple(100, ' explosion ' ) ;
this . explosionPool . setAl 1 ( ' anchor . x ' , 0.5);
this . explosionPool . setAl 1 ( ' anchor . y 1 , 0.5);
this . explosionPool . forEach( function (explosion) {
explosion . animations . add( ' boom ' ) ;
});
this .cursors = this . input . keyboard . createCursorKeys( ) ;
Then create a new function:
161
162
163
164
165
166
167
168
169
170
171
Afternoon 3: Object Groups
33
explode: function (sprite) {
if (this . explosionPool . countDead( ) === 0) {
return;
}
var explosion = this . explosionPool . getFirstExists( false) ;
explosion . reset (sprite . x, sprite . y ) ;
explosion . play (' boom ' , 15, false, true);
// add the original sprite's velocity to the explosion
explosion . body . velocity . x = sprite . body . velocity . x;
explosion . body . velocity . y = sprite. body. velocity .y;
},
And refactor the collision callbacks:
enemyHit: function (bullet, enemy) {
bullet.killQ;
this .explode(enemy) ;
enemy . ki 1 1 ( ) ;
var explosion = this . add . sprite(onomy . x, — onomy . y , — ' explosion ' ) ;
e xplosion . anchor . s c tTo(0 .5, 0.5);
explosion . animations . add( ' boom ' ) ;
e xplosion . play (' boom ' , 15, — fals e , tru e );
},
playerHit: function (player, enemy) {
this .explode(enemy) ;
enemy . ki 1 1 ( ) ;
— var explosion = this . add . sprite(playor . x, — player . y , — ' explosion ' ) ;
— e xplosion . anchor . s c tTo(0 .5, 0.5);
— explosion . animations . add( ' boom ' ) ;
— e xplosion . play (' boom ' , 15, — fals e , tru e );
this.explode(player) ;
player . ki 1 1 ( ) ;
},
Afternoon 3: Object Groups
34
Sprite Ordering
We mentioned before that the ordering of sprites is determined by the time they are added into our
game i.e. the first objects (sprites, text, etc) added are at the bottom while the later objects are at the top.
This is done through sprite groups: all objects (sprites, text, and even groups - groups can contain other
groups) are added to the game’s World by default, a special group in our game. Display order is then
determined by iterating over the members of the World.
For example, the order of the contents of World in the following scene is:
• The sea tile sprite is at the bottom.
• The player sprite is next.
• The greenEnemy sprite group is on the next level. Only a few sprites from this group are visible
(the rest are still dead).
• Next is the bullet sprite group. Same as the enemy group, only a few sprites from this group are
visible.
• Next is the explosion sprite group.
• At the top is the instructions text. It’s not visible anymore at this point in the game.
(World is also contained in the Stage but we won’t be using the Stage directly so we won’t cover it.)
15
16
17
18
19
20
21
22
23
24
Intermission: Refactoring
Before we proceed with the rest of the lessons, let’s refactor the code to make it easier for us to change and
maintain the code later. This should not change the behavior of the game, so this is just an intermission
rather than a full afternoon chapter.
Refactoring Functions
First on our list of things to refactor are our create( ) and update( ) functions. They’re getting bigger and
they will be worse as we proceed with the workshop. We’ll refactor them by splitting these large functions
into smaller functions.
Function Order
There’s no generally accepted standard for ordering functions within classes. Modern editors and IDEs
have features (e.g. quick search, code folding) that allow devs to order functions any way they like.
For our program, our standard will be to group functions according to their usage. This will reduce the
amount of scrolling needed when editing multiple functions.
Here is the general outline of our game . js after our refactoring:
• Phaser game loop functions
• Functions called by create ( )
• Functions called by update ( )
Refactoring create
Let’s start by extracting functions out of create ( ). Replace the contents of the function with:
create: function () {
this . setupBackground( ) ;
this . setupPlayer( ) ;
this . setupEnemies( ) ;
this . setupBul lets( ) ;
this . setupExplosions( ) ;
this . setupText( ) ;
this .cursors = this . input . keyboard . createCursorKeys( ) ;
},
Then insert the following after render ( ) :
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
Intermission: Refactoring
36
//
// create( ) - related functions
//
setupBackground function () {
this .sea - this . add . ti leSprite(0, 0, 800, 600, 'sea');
this . sea . autoScrol 1 (0, 12);
},
setupPlayer function () {
this. player = this . add . sprite(400, 550, 'player');
this . player . anchor . setTo(0 . 5, 0.5);
this . player . animations . add( ' fly ' , [0, 1, 2 ], 20, true);
this . player ,play( ' fly' );
this . physics . enable(this . player, Phaser . Physics . ARCADE) ;
this . player . speed = 300;
this . player . body . col 1 ideWorldBounds = true;
// 20 x 20 pixel hitbox , centered a little bit higher than the center
this . player . body . setSize(20, 20, 0, -5);
},
setupEnemies : function () {
this .enemyPool = this . add . group( ) ;
this . enemyPool . enableBody = true;
this . enemyPool . physicsBodyType Phaser . Physics . ARCADE;
this . enemyPool . createMultiple(50, ' greenEnemy ' ) ;
this . enemyPool . setAl 1 ( ' anchor . x ' , 0.5);
this . enemyPool . setAl 1 ( ' anchor . y ' , 0.5);
this . enemyPool . setAl 1 ( ' outOfBoundsKi 11', true) ;
this . enemyPool . setAl 1 ( ' checkWor ldBounds ' , true) ;
// Set the animation for each sprite
this . enemyPool . forEach( function (enemy) {
enemy . animations . add( ' fly ' , [0, 1, 2 ], 20, true);
});
this . nextEnemyAt = 0;
this . enemyDelay = 1000;
},
setupBullets function () {
// Add an empty sprite group into our game
this . bul letPool - this . add . group( ) ;
// Enable physics to the whole sprite group
this . bul letPool . enableBody = true;
this . bul letPool . physicsBodyType Phaser . Physics . ARCADE ;
// Add 100 'bullet' sprites in the group.
// By default this uses the first frame of the sprite sheet and
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
Intermission: Refactoring
37
// sets the initial state as non-existing (i.e. killed/dead)
this . bul letPool . createMultiple(100, 'bullet' ) ;
// Sets anchors of all sprites
this . bul letPool . setAl 1 ( ' anchor . x ' , 0.5);
this . bul letPool . setAl 1 ( ' anchor . y ' , 0.5);
// Automatically kill the bullet sprites when they go out of bounds
this . bul letPool . setAl 1 ( ' outOfBoundsKi 11', true) ;
this . bul letPool . setAl 1 ( ' checkWor ldBounds ' , true) ;
this . nextShotAt = 0;
this . shotDelay = 100;
},
setupExplosions : function () {
this . explosionPool = this . add . group( ) ;
this . explosionPool . enableBody = true;
this . explosionPool . physicsBodyType = Phaser .Physics. ARCADE;
this . explosionPool . createMultiple(100, ' explosion ' ) ;
this . explosionPool . setAl 1 ( ' anchor . x ' , 0.5);
this . explosionPool . setAl 1 ( ' anchor . y ' , 0.5);
this . explosionPool . forEach( function (explosion) {
explosion . animations . add( ' boom ' ) ;
});
},
setupText function () {
this . instructions = this . add . text( 400, 500,
'Use Arrow Keys to Move, Press Z to Fire\n' +
'Tapping/clicking does both',
{ font: ’ 20px monospace’, fill: '#fff', align 'center' }
);
this . instructions . anchor . setTo(0 . 5, 0.5);
this . instExpire = this . time . now + 10000;
},
We also added a call to this, sea . autoScrol 1 ( ) so that we can remove the this .sea . ti lePosition . y
+=0.2 from the update( ) later.
Refactoring update
Now replace the contents of update( ) with the following:
26
27
28
29
30
31
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
Intermission: Refactoring
38
update: function () {
this . checkCol 1 isions( ) ;
this . spawnEnemies( ) ;
this . processPlayerInput( ) ;
this . processDelayedEf fects( ) ;
},
Insert the new functions after the create ( ) functions:
//
// update( ) - related functions
//
checkCol 1 isions : function () {
this . physics . arcade . overlap(
this . bul letPool , this . enemyPool , this . enemyHit, null, this
);
this . physics . arcade . overlap(
this. player, this . enemyPool , this . playerHit, null, this
);
},
spawnEnemies function () {
if (this . nextEnemyAt < this . time . now && this . enemyPool . countDead( ) > 0) {
this . nextEnemyAt = this . time . now + this . enemyDelay ;
var enemy = this . enemyPool . getFirstExists( false) ;
// spawn at a random location top of the screen
enemy . reset(this . rnd . integer InRange(20, 780), 0);
// also randomize the speed
enemy . body . velocity . y = this . rnd . integer InRange(30, 60);
enemy . play( ' fly 1 ) ;
}
},
processPlayer Input : function () {
this . player . body . velocity . x = 0;
this . player . body . velocity . y = 0;
if (this. cursors. left. isDown) {
this . player . body . velocity . x = -this . player . speed;
} else if (this . cursors . right . isDown) {
this . player . body . velocity . x = this . player . speed;
}
if (this . cursors . up . isDown) {
this . player . body . velocity . y = -this . player . speed;
} else if (this . cursors . down . isDown) {
this . player . body . velocity . y = this . player . speed;
}
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
Intermission: Refactoring
39
if (this . input . activePointer . isDown &&
this . physics . arcade . distanceToPointer (this . player ) > 15) {
this . physics . arcade . moveToPo inter (this . player, this . player . speed) ;
}
if (this . input . keyboard . isDown(Phaser . Keyboard . Z) II
this . input . activePointer . isDown) {
this . f ire( ) ;
}
},
processDelayedEf fects : function () {
if (this . instructions . exists && this . time . now > this . instExpire) {
this . instructions . destroy( ) ;
}
},
Reducing Hard-coded Values
Apart from long functions, our game also has many hard-coded values and this may affect code readability
and maintenance later.
Eliminating all hard-coded values would be overkill especially for a tutorial like this, so our goal here
would be show the ways how we could reduce them.
Using Relative Values
A good portion of the hard-coded values are x-y coordinates. Replacing them with values relative to
game, width and game, height will allow us to change the size of the game later with minimal impact to
the code.
Let’s start with the background tile sprite:
setupBackground function () {
this. sea = this . add . ti leSprito(0, — Q-, — 800, — 600, — ' sea ' ) ;
this. sea = this. add. tileSprite(0, 0, this. game. width, this . game . height, 'sea');
this . sea . autoScrol 1 (0, 12);
},
Then we change the player starting location to the bottom middle of the screen:
setupPlayer function () {
this. player = this. add. spritc(400, — 550, — ' player ' ) ;
this. player = this. add. sprite(this. game. width / 2, this . game. height - 50, 'player');
this . player . anchor . setTo(0 . 5, 0.5);
Also the instruction text:
Intermission: Refactoring
40
setupText: function () {
this . instructions = this . add . tcxt( — 400, — 500,
this . instructions = this . add . text(
this . game . width / 2,
this . game . height - 100,
'Use Arrow Keys to Move, Press Z to Fire\n' +
And finally the spawn location for the enemies:
spawnEnemies : function () {
if (this . nextEnemyAt < this . time . now && this . enemyPool . countDead( ) > 0) {
this . nextEnemyAt = this . time . now + this . enemyDelay ;
var enemy = this . enemyPool . getFirstExists( false) ;
// spawn at a random location top of the screen
enemy . reset ( this . rnd . integer I nRango( 20, — 780) , — 0-)-r
enemy . reset(this . rnd . integerInRange(20, this . game . width - 20), 0);
// also randomize the speed
enemy . body . velocity . y = this . rnd . integer InRange(30, 60);
One advantage of using relative values is that we can change the dimensions of the game without having
to change any of the code. For example, here’s the game with the height and width flipped at app . js:
Using Constants
We can also replace many hard-coded values with constants. If you open boot, js, you’ll see that all of
the constants that we need for this workshop are already defined under the BasicGame object. All we need
to do is to replace the existing code with their respective constants:
Intermission: Refactoring
41
setupBackground : function () {
this .sea = this . add . ti leSprite(0, 0, this. game. width, this . game . height ,
— this . sea . autoScrol 1(0, — 12) ;
this . sea . autoScrol 1(0, BasicGame . SEA_SCROLL_SPEED) ;
},
setupPlayer function () {
this . physics . enable(this . player, Phaser . Physics . ARCADE) ;
— this . play e r . sp ee d = 300;
this . player . speed = BasicGame . PLAYER_SPEED;
this . player . body . col 1 ideWorldBounds = true;
setupEnemies : function () {
this . nextEnemyAt = 0;
— this . cnemyDolay = 1000;
this. enemy Del ay = BasicGame. SPAWN_ENEMY_DELAY;
},
setupBullets function () {
this . nextShotAt = 0;
— this . shotD c lay = 100;
this . shotDelay = BasicGame. SHOT_DELAY;
},
setupText : function () {
this . instructions . anchor . setTo(0 . 5, 0.5);
— this . instExpirc = this . time . now + 10000;
this . instExpire = this. time. now + BasicGame . INSTRUCTION_EXPIRE;
},
spawnEnemies : function () {
if (this . nextEnemyAt < this . time . now && this . enemyPool . countDead( ) > 0)
// also randomize the speed
e n e my . body . v e locity . y = this . rnd . int e g e r InRang c (30, 60);
enemy . body . velocity . y = this . rnd . integerInRange(
BasicGame. ENEMY_MIN_Y_VELOCITY, BasicGame . ENEMY_MAX_Y_VELOCITY
);
enemy . play( ' fly' ) ;
}
' sea ' ) ;
Intermission: Refactoring
42
fire: function () {
— bul let . body . velocity . y ~ — 509;
bullet. body. velocity .y = BasicGame. BULLET_VELOCITY;
},
Afternoon 4: Health, Score, and Win/Lose
Conditions
Our game looks more like a real game now, but there’s still a lot of room for improvement.
Enemy Health
Phaser makes modifying enemy toughness easy for us because it supports health and damage calculation.
Before we could implement health to our enemies, let’s first add a hit animation (finally using the last
frame of the sprite sheet):
setupEnemies : function () {
this . enemyPool . setAl 1 ( ' checkWor ldBounds ' , true)
// Set the animation for each sprite
this . enemyPool . forEach( function (enemy) {
enemy . animations . add( 1 fly ' , [ 0, 1, 2 ], 20, true);
enemy . animations . add( ' hit ' , [ 3, 1, 3, 2 ], 20, false);
enemy .events. onAnimationComplete.add( function (e) {
e.playf ' fly ' );
}, this);
});
this . nextEnemyAt = 0;
The new animation is a very short non-looping blinking animation which goes back to the original fly
animation once it ends.
Let’s now add the health. Sprites in Phaser have a default health value of 1 but we can override it anytime:
spawnEnemies : function () {
if (this . nextEnemyAt < this . time . now && this . enemyPool . countDead( ) > 0) {
this . nextEnemyAt = this . time . now + this.enemyDelay;
var enemy = this . enemyPool . getFirstExists( false) ;
// spawn at a random location top of the screen
e n e my . r e s e t (this . rnd . int c g c rInRang c (20, — this . gam e . width 20) , — 0) ;
enemy . reset(
this . rnd . integerInRange(20, this . game . width - 20), 0,
BasicGame. ENEMY_HEALTH
);
enemy . body . velocity . y = this . rnd . integer InRange(
BasicGame . ENEMY_MIN_Y_VELOCITY, BasicGame . ENEMY_MAX_Y_VELOCITY
Afternoon 4: Health, Score, and Win/Lose Conditions
44
203
204
205
206
207
208
209
210
);
enemy . play( ' fly' ) ;
}
},
We could have used enemy. health = BasicGame . ENEMYJHEALTH but reset() already has an optional
parameter that does the same.
And finally, let’s create a new function to process the damage, centralizing the killing and explosion
animation:
enemyHit: function (bullet, enemy) {
bullet.killQ;
this . exp lode (enemy) ;
enemy . ki 1 1 ( ) ;
this . damageEnemy (enemy, BasicGame . BULLET_DAMAGE) ;
},
playerHit: function (player, enemy) {
— this . exp lode (enemy) ;
— e n e my . ki 1 1 ( ) ;
// crashing into an enemy only deals 5 damage
this . damageEnemy(enemy , BasicGame . CRASH_DAMAGE) ;
this . exp 1 ode ( player) ;
player . ki 1 1 ( ) ;
},
damageEnemy: function (enemy, damage) {
enemy . damage(damage) ;
if (enemy . al ive) {
enemy . play( ' hit ' ) ;
} else {
this . explode(enemy ) ;
}
},
Using damage ( ) automatically ki 1 1 ( ) s the sprite once its health is reduced to zero.
Afternoon 4: Health, Score, and Win/Lose Conditions
45
Player Score
We don’t need to explain how important it is to display the player’s current score on the screen. Everyone
just knows it.
First set the score rewarded on kill:
setupEnemies : function () {
this . enemyPool . setAl 1 ( ' outOfBoundsKi 11', true) ;
this . enemyPool . setAl 1 ( ' checkWor ldBounds ' , true) ;
this . enemyPool . setAll (' reward ' , BasicGame . ENEMY_REWARD, false, false, 0, true);
// Set the animation for each sprite
this . enemyPool . forEach( function (enemy) {
We used the full form of the setAl 1 ( ) function. The last four parameters are default, and we only change
the last parameter to true which forces the function to set the reward property even though it isn’t there.
Next step is to add the setupText( ) code for displaying the starting score:
setupText: function () {
this . instructions = this . add . text(
this . game . width / 2,
this . game . height - 100,
'Use Arrow Keys to Move, Press Z to Fire\n' +
'Tapping/clicking does both',
{ font '20px monospace', fill '#fff', align 'center' }
);
this . instructions . anchor . setTo(0 . 5, 0.5);
this . instExpire = this . time . now + BasicGame. INSTRUCTION_EXPIRE;
this. score = 0;
this . scoreText = this . add . text(
this . game . width / 2, 30, ' ' + this. score,
{ font: '20px monospace', fill '*fff', align 'center' }
);
this . scoreText . anchor . setTo(0 . 5, 0.5);
},
And then let’s add it to our enemy damage/death handler:
Afternoon 4: Health, Score, and Win/Lose Conditions
46
damageEnemy : function (enemy, damage) {
enemy . damage(damage) ;
if (enemy . al ive) {
enemy . play( 'hit' ) ;
} else {
this . explode(enemy ) ;
this . addToScore(enemy . reward) ;
}
},
addToScore: function (score) {
this. score += score;
this . scoreText . text = this. score;
224 },
Player Lives
Sudden death games are cool, but may be “unfun” for others. Most people are used to having lives and
retries in their games.
First, let’s create a new sprite group representing our lives at the top right corner of the screen.
create: function () {
this . setupBackground( ) ;
this . setupPlayer( ) ;
this . setupEnemies( ) ;
this . setupBul lets( ) ;
this . setupExplosions( ) ;
this . setupPlayerIcons( ) ;
this . setupText( ) ;
this .cursors = this . input . keyboard . createCursorKeys( ) ;
118
119
120
121
122
123
124
125
126
127
Afternoon 4: Health, Score, and Win/Lose Conditions
47
},
setupPlayerlcons : function () {
this . 1 ives = this . add . group( ) ;
// calculate location of first life icon
var f irstLi felconX this . game . width - 10 - (BasicGame.PLAYER_EXTRA_LIVES * 30);
for (var i = 0; i < BasicGame . PLAYER_EXTRA_LIVES ; i++) {
var 1 i f e = this . 1 ives . create( f irstLi felconX + (30 * i), 30, 'player');
1 i fe . scale . setTo(0 . 5, 0.5);
1 i fe . anchor . setTo(0 . 5, 0.5);
}
},
For the life icons, we just used the player’s sprite and scaled it down to half its size by modifying the
scale property.
With the life tracking done, let’s add the blinking ghost animation on player death:
this . player . animations . add( ' fly ' , [0, 1, 2 ], 20, true);
this . player . animations . add( ' ghost ' , [ 3, 0, 3, 1 ], 20, true);
this . player ,play( ' fly' );
Then let’s modify playerHit( ) to activate “ghost mode” for 3 seconds and ignore everything around us
while we’re a ghost:
playerHit: function (player, enemy) {
// check first if this . ghostUntil is not not undefined or null
if (this . ghostllnti 1 && this . ghostUnti 1 > this .time. now) {
return;
}
// crashing into an enemy only deals 5 damage
this . damageEnemy (enemy, BasicGame . CRASH_DAMAGE) ;
this . cxplodo( player) ;
player . ki 1 1 ( ) ;
var life = this . lives . getFirstAlive( ) ;
if (life !== null) {
li fe. kill ( ) ;
this. ghostUntil = this. time. now + BasicGame . PLAYER_GHOST_TIME;
this . player . play ( ' ghost ' ) ;
} else {
this.explode(player) ;
player . kil 1 ( ) ;
}
},
And finally, we modify the processDelayedEf fects( ) function to check if the ghost mode has already
expired:
255
256
257
258
259
260
261
262
263
264
265
266
Afternoon 4: Health, Score, and Win/Lose Conditions
48
processDelayedEf fects : function () {
if (this . instructions . exists && this . time . now > this . instExpire) {
this . instructions . destroy( ) ;
}
if (this . ghostllnti 1 && this . ghostUnti 1 < this .time. now) {
this.ghostUntil = null;
this . player .pi ay ('fly');
}
Win/Lose Conditions, Go back to Menu
One of the last things we need to implement is a game ending condition. Currently, our player can die,
but there’s no explicit message whether the game is over or not. On the other hand, we also don’t have a
“win” condition.
Let’s implement both to wrap up our prototype.
Create a new function to display the end game message:
displayEnd function (win) {
// you can't win and lose at the same time
if (this . endText && this . endText . exists) {
return;
}
var msg = win ? 'You Win! ! ! ' 'Game Over! ' ;
this. endText = this . add . text(
this . game . width / 2, this . game . height / 2 - 60, msg,
{ font: '72px serif', fill ' # f f f ' }
);
this . endText . anchor . setTo(0 . 5, 0) ;
Afternoon 4: Health, Score, and Win/Lose Conditions
49
267
this . showReturn this . time . now + BasicGame . RETURN_MESSAGE_DELAY;
269 },
Modify the playerHit( ) function to call the “Game Over!” message:
playerHit: function (player, enemy) {
} else {
this.explode(pl ayer ) ;
player . ki 1 1 ( ) ;
this . displayEnd( false) ;
}
Do the same to the addToScore( ) function, but now to destroy all enemies (preventing accidental death
and also stopping them from spawning) and display “You Win!!!” message upon reaching 2000 points:
addToScore: function (score) {
this. score += score;
this . scoreText . text this. score;
if (this. score >= 2000) {
this . enemyPool . destroy ( ) ;
this . displayEnd(true) ;
}
},
(No need to set 2000 as a constant because it’s only a temporary placeholder. We’ll change this value in
the last afternoon chapter.)
Let’s also display a “back to main menu” message a few seconds after the game ends. In processDelayed-
Ef fects( ):
this . player ,play( 'fly' );
}
if (this . showReturn && this . time . now > this . showReturn) {
this . returnText = this . add . text(
this . game . width / 2, this . game . height / 2 + 20,
'Press Z or Tap Game to go back to Main Menu',
{ font: '16px sans-serif', fill '*fff'}
);
this . returnText . anchor . setTo(0 . 5, 0.5);
this . showReturn = false;
}
},
Since our main menu button is the same action as firing bullets, we can modify processPlayerInput( )
function to allow us to quit the game:
Afternoon 4: Health, Score, and Win/Lose Conditions
50
if (this . input . keyboard . isDown(Phaser . Keyboard . Z) II
this. input . activePointer . isDown) {
this . f iro( ) ;
if (this . returnText && this. returnText. exists) {
this . quitGame( ) ;
} else {
this . f ire( ) ;
}
}
},
Before going back to the main menu, let’s destroy all objects in the world to allow us to play over and
over again:
quitGame: function (pointer) {
// Here you should destroy anything you no longer need.
// Stop music, delete sprites, purge caches, free resources, all that good stuff.
this . sea . destroy( ) ;
this . player . destroy ( ) ;
this . enemyPool . destroy ( ) ;
this.bulletPool .destroy();
this . explosionPool . destroy ( ) ;
this . instructions . destroy ( ) ;
this . scoreText . destroy ( ) ;
this . endText . destroy ( ) ;
this . returnText . destroy ( ) ;
// Then let's go back to the main menu.
this . state . start( ' MainMenu ' ) ;
}
Going back to the main menu will display a black screen with text. This is because we skipped loading
the title page image in preloader . js. To properly display the main menu, let’s temporarily add the pre-
loading in mainMenu . js:
BasicGame . MainMenu . prototype = {
preload function () {
this . load . image( ' titlepage ' , ' assets/titlepage . png ' ) ;
},
create: function () {
Enjoy playing your prototype game!
Afternoon 4: Health, Score, and Win/Lose Conditions
51
Game Over screen
Win screen
Afternoon 4: Health, Score, and Win/Lose Conditions
pressing Z returns you to the Main Menu
Afternoon 5: Expanding the Game
Let’s flesh out the game by adding an additional enemy, a power-up, a boss battle, and sounds.
Harder Enemy
The green enemy fighters in our current game pose no real threat to our players. To make our game
difficult, our next enemy type will be able to shoot at our player while also being faster and toughe
Enemy Setup
First load the sprite sheet in the pre-loader:
preload: function () {
this . load . image( ' sea ' , ' assets/sea . png ' ) ;
this . load . image( ' bul let ' , 1 assets/bul let . png ' ) ;
this . load . spritesheet( ' greenEnemy ' , 1 assets/enemy . png ' , 32, 32);
this . load . spritesheet( ' whiteEnemy ' , 1 assets/shooting-enemy . png ' , 32, 32);
this . load . spritesheet( ' explosion ' , 1 assets/explosion . png 1 , 32, 32);
this . load . spritesheet( ' player ' , ' assets/player . png ' , 64, 64);
},
Then create the group for the sprites (which we will call “shooters” from now on):
setupEnemies : function () {
this . enemy Del ay = BasicGame . SPAWN_ENEMY_DELAY;
this . shooterPool = this . add . group( ) ;
this . shooterPool . enableBody = true;
this . shooterPool . physicsBodyType = Phaser. Physics. ARCADE;
this . shooterPool .createMultiple(20, 'whiteEnemy' );
this . shooterPool .setAll( 'anchor. x' , 0.5);
this . shooterPool .setAll( ' anchor. y ' , 0.5);
this . shooterPool .setAll( 'outOfBoundsKill ' , true);
this . shooterPool .setAll( ' checkWorldBounds ' , true);
this . shooterPool .setAll(
'reward', BasicGame . SHOOTER_REWARD, false, false, 0, true
);
// Set the animation for each sprite
this . shooterPool . forEach( function (enemy) {
enemy . animations . add( ' fly ' , [0, 1, 2 ], 20, true);
Afternoon 5: Expanding the Game
54
enemy . animations . add( ' hit ' , [ 3, 1, 3, 2 ], 20, false);
enemy .events. onAnimationComplete. add ( function (e) {
e.play( ' fly ' );
}, this);
});
// start spawning 5 seconds into the game
this . nextShooterAt = this . time . now + Phaser .Timer. SECOND * 5;
this . shooterDelay = BasicGame . SPAWN_SHOOTER_DELAY ;
},
Diagonal Movement
Instead of moving only downwards like the regular enemy, we’ll make the shooters move diagonally
across the screen. Add the following code into spawnEnemies( ):
spawnEnemies : function () {
enemy . play( 1 f ly ' ) ;
}
if (this . nextShooterAt < this . time . now && this . shooterPool .countDead( ) > 0) {
this . nextShooterAt = this . time . now + this. shooterDelay;
var shooter = this . shooterPool . getFirstExists( false) ;
// spawn at a random location at the top
shooter . reset(
this . rnd . integerInRange(20, this . game . width - 20), 0,
BasicGame. SHOOTER_HEALTH
);
// choose a random target location at the bottom
var target = this . rnd . integerInRange(20, this . game . width - 20);
// move to target and rotate the sprite accordingly
shooter . rotation = this . physics . arcade . moveToXY(
shooter, target, this . game . height,
this . rnd . integer I nRange(
BasicGame. SHOOTER_MIN_VELOCITY, BasicGame . SHOOTER_MAX_VELOCITY
)
) - Math. PI / 2;
shooter .pi ay ('fly');
// each shooter has their own shot timer
shooter . nextShotAt = 0;
}
Afternoon 5: Expanding the Game
55
The figure above shows the initial spawn and target areas for the shooters; the arrows show possible flight
paths. Here we’re using moveToXY( ), a function similar to moveToPointer( ) which moves the object to a
given point in the world.
Both moveToPo i nter ( ) and moveToXY ( ) returns the angle towards the target in radians, and we can assign
this value to object . rotation to rotate our sprite towards the target. But applying the value directly will
result in incorrectly oriented shooters:
This is because Phaser assumes that your sprites are oriented to the right. We rotated our sprite
counterclockwise Math .PI / 2 radians (90 degrees) to compensate for the fact that our sprite is oriented
downwards.
Afternoon 5: Expanding the Game
56
expected sprite
actual sprite
Angles/Rotation in Phaser
Angles in Phaser are same as in Trigonometry, though it might look wrong at first glance for those used
to Cartesian coordinates rather than screen coordinates.
angles in Cartesian coordinate system angles in screen coordinate system
The rotation seems flipped (increasing angles are clockwise rotations rather than counterclockwise)
because the y values for the two coordinate systems are flipped.
By the way, you can use object . angle instead of object . rotation if you prefer rotating in degrees rather
than radians.
Shooting
Setting up the bullets are pretty much the same as the regular bullets. First the preload( ):
Afternoon 5: Expanding the Game
57
preload: function () {
this . load . image( 1 sea ' , ' assets/sea . png ' ) ;
this . load . image( ' bul let ' , ' assets/bul let . png ' ) ;
this . load . image ( ' enemy Bul let ' , ' assets/enemy -bul let . png ' ) ;
this . load . spritesheet( ' greenEnemy ' , 1 assets/enemy . png ' , 32, 32);
this . load . spritesheet( ' whiteEnemy ' , 1 assets/shooting-enemy . png ' , 32, 32);
this . load . spritesheet( ' explosion ' , 1 assets/explosion . png 1 , 32, 32);
this . load . spritesheet( ' player ' , ' assets/player . png ' , 64, 64);
},
Then the sprite group at setupBul lets( ):
setupBul lets : function () {
this . enemyBul letPool = this . add . group( ) ;
this . enemyBul letPool . enableBody = true;
this . enemyBul letPool . physicsBodyType = Phaser. Physics. ARCADE;
this . enemyBul letPool . createMultiple(100, 'enemyBullet' ) ;
this . enemyBul letPool ,setAll( 'anchor. x' , 0.5);
this . enemyBul letPool ,setAll( 'anchor. y ' , 0.5);
this . enemyBul letPool ,setAll( 'outOfBoundsKill ' , true);
this . enemyBul letPool ,setAll( ' checkWorldBounds ' , true);
this. enemyBul letPool ,setAll( 'reward' , 0, false, false, 0, true);
// Add an empty sprite group into our game
this . bul letPool = this . add . group( ) ;
We’ve already set the shot timer for the individual shooters in the spawning section. All that’s left is to
create a new function that fires the enemy bullets.
update: function () {
this . checkCol 1 isions( ) ;
this . spawnEnemies( ) ;
this . enemy Fire( ) ;
this . processPlayer Input( ) ;
this . processDelayedEf fects( ) ;
},
And the actual function, iterating over the live shooters in the world:
244
245
246
247
248
249
250
251
252
253
254
255
Afternoon 5: Expanding the Game
58
enemyFire: function() {
this . shooterPool . forEachAl ive( function (enemy) {
if (this . time . now > enemy . nextShotAt && this . enemyBul letPool . countDead( ) > 0) {
var bullet = this . enemyBul letPool . getFirstExists( false) ;
bul let . reset(enemy . x, enemy.y);
this . physics . arcade . moveToObject(
bullet, this. player, BasicGame . ENEMY_BULLET_VELOCITY
);
enemy . nextShotAt = this . time . now + BasicGame . SHOOTER_SHOT_DELAY;
}
}, this);
},
Collision Detection
To wrap things up, let’s handle the collisions for the shooters as well as their bullets:
checkCol 1 isions : function () {
this . physics . arcade . overlap(
this . bul letPool , this . enemyPool , this . enemyHit, null, this
);
this . physics . arcade . overlap(
this.bulletPool, this . shooterPool , this . enemyHit, null, this
);
this . physics . arcade . overlap(
this. player, this . enemyPool , this . playerHit, null, this
);
this . physics . arcade . overlap(
this. player, this . shooterPool , this . playerHit, null, this
);
this . physics . arcade . overlap(
this. player, this.enemyBulletPool, this . playerHit, null, this
);
},
We’ll also destroy the shooters and bullets in addToScore( ) upon winning:
Afternoon 5: Expanding the Game
59
addToScore: function (score) {
this. score += score;
this . scoreText . text = this. score;
if (this. score >= 2000) {
this . enemyPool . destroy( ) ;
this . shooterPool . destroy ( ) ;
this . enemyBul letPool . destroy ( ) ;
this . displayEnd(true) ;
}
},
Afternoon 5: Expanding the Game
60
Power-up
Our regular bullet stream is now a lot weaker with the introduction of the shooters. To counter this, let’s
add a power-up that our players can pickup to get a spread shot.
Pre-loading the asset:
preload: function () {
this . load . image( ' sea ' , ' assets/sea . png ' ) ;
this . load . image( ' bul let 1 , ' assets/bul let . png ' ) ;
this . load . image( 1 enemyBul let ' , ' assets/enemy-bul let . png 1 ) ;
this . load . image( ' powerupl 1 , ' assets/powerupl . png ' ) ;
this . load . spritesheet( ' whiteEnemy ' , 1 assets/shooting-enemy . png ' , 32, 32);
Then creating the sprite group:
setupPlayer Icons : function () {
this . powerllpPool = this . add . group( ) ;
this . powerUpPool . enableBody = true;
this . powerUpPool . physicsBodyType = Phaser. Physics. ARCADE;
this . powerUpPool .createMultiple(5, ' powerupl ' ) ;
this . powerUpPool . setAll ( ' anchor . x ' , 0.5);
this . powerUpPool . setAll ( ' anchor . y ' , 0.5);
this . powerUpPool . setAll ( ' outOfBoundsKi 11 ' , true) ;
this . powerUpPool . setAll ( ' checkWorldBounds ' , true) ;
this . powerUpPool . setAll (
'reward', BasicGame . POWERUP_REWARD, false, false, 0, true
);
this . 1 ives = this . add . group( ) ;
We also add the possibility of spawning a power-up when an enemy dies, 30% chance for regular enemies
and 50% for shooters:
414
415
416
417
418
419
420
421
422
423
424
Afternoon 5: Expanding the Game
61
setupEnemies function () {
this . enemyPool . setAl 1 (' reward ' , BasicGame . ENEMY_REWARD, false, false, 0, true);
this .enemyPool ,setAll(
'dropRate', BasicGame. ENEMY_DROP_RATE, false, false, 0, true
);
this . shooterPool . setAl 1 (
'reward', BasicGame . SHOOTER_REWARD, false, false, 0, true
);
this . shooterPool .setAll(
'dropRate', BasicGame . SHOOTER_DROP_RATE, false, false, 0, true
);
Add the call in damageEnemy( ) to a function that spawns power-ups:
damageEnemy: function (enemy, damage) {
enemy . damage(damage) ;
if (enemy . al ive) {
enemy . play( 'hit' ) ;
} else {
this . explode(enemy ) ;
this . spawnPowerllp(enemy ) ;
this . addToScore( enemy . reward) ;
}
},
Here’s the new function for spawning power-ups:
spawnPowerllp function (enemy) {
if (this . powerUpPool . countDead( ) === 0 II this . weaponLevel === 5) {
return ;
}
if (this . rnd . frac( ) < enemy . dropRate) {
var powerUp = this . powerUpPool . getFirstExists( false) ;
powerUp . reset(enemy . x, enemy.y);
powerUp . body . velocity . y = BasicGame . POWERUP_VELOCITY;
}
},
Weapon levels
You might have noticed the this . weaponLevel == 5 in the last code snippet. Our weapon strength will
have up to 5 levels, each incremented by picking up a power-up.
Setting the initial value to zero:
Afternoon 5: Expanding the Game
62
391
392
393
394
395
396
397
setupPlayer function () {
this . player . body . setSize(20, 20, 0, -5);
this .weaponLevel = 0;
},
Adding a collision handler:
checkCol 1 isions : function () {
this . physics . arcade . overlap(
this . bul letPool , this . enemyPool , this . enemyHit, null, this
);
this . physics . arcade . overlap(
this. player, this . powerllpPool , this . playerPowerUp, null, this
);
},
And a new function for incrementing the weapon level:
playerPowerUp function (player, powerUp) {
this . addToScore( powerUp . reward) ;
powerUp . ki 1 1 ( ) ;
if (this . weaponLevel < 5) {
this . weaponLevel++;
}
},
A common theme in shoot ‘em ups is that your weapon power resets when you die. Let’s add that into
our code:
playerHit: function (player, enemy) {
if (life ! == null) {
life.killQ;
this . weaponLevel = 0;
this . ghostUnti 1 = this . time . now + BasicGame.PLAYER_GHOST_TIME;
And finally, the code for implementing the spread shot:
Afternoon 5: Expanding the Game
63
fire: function() {
if (! this . player . al ive || this . nextShotAt > this . time . now) {
return;
}
— if (this . bul I c tPool . countD c ad( ) === 0) — 0
return;
this . nextShotAt = this . time . now + this . shotDelay ;
— // Find the first dead bullet in the pool
— var bullet = this . bul letPool . gctFirstExists( false) ;
— // Reset (revive) the sprite and place it in a new location
— bullet. reset (this . player . x, — this . player . y 20) ;
— bul let. body. velocity. y = BasicGamo . BULLET_VELOCITY ;
var bullet;
if (this . weaponLevel === 0) {
if (this . bul letPool . countDead( ) === 0) {
return ;
}
bullet = this . bulletPool .getFirstExists(false);
bullet. reset(this. player. x, this . player . y - 20);
bul let. body .velocity .y = BasicGame. BULLET_VELOCITY ;
} else {
if (this . bul letPool . countDead( ) < this . weaponLevel * 2) {
return ;
}
for (var i = 0; i < this . weaponLevel ; i++) {
bullet = this. bulletPool .getFirstExists(false);
// spawn left bullet slightly left off center
bullet. reset(this. player. x - (10 + i * 6), this. player. y - 20);
// the left bullets spread from -95 degrees to -135 degrees
this . physics . arcade . velocityFromAngle(
-95 - i * 10, BasicGame. BULLET_VELOCITY, bul let . body . velocity
);
bullet = this. bulletPool .getFirstExists(false);
// spawn right bullet slightly right off center
bullet. reset(this. player. x + (10 + i * 6), this. player. y - 20);
// the right bullets spread from -85 degrees to -45
this . physics . arcade . velocityFromAngle(
-85 + i * 10, BasicGame. BULLET_VELOCITY, bul let . body . velocity
);
}
Afternoon 5: Expanding the Game
64
},
One last thing before you test your new spread shot: let’s increase the win condition to 20,000 points so
that the game will not end before you can see your new weapon in all its greatness:
if (this. score >~ 2000) — f-
if (this. score >= 20000) {
Note that it’s you can run out of available bullet sprites as shown with the bullet gaps above. You can
avoid this by increasing the amount of bullet sprites created in the setupBul lets( ) function, but it’s not
really that necessary gameplay-wise.
Afternoon 5: Expanding the Game
65
Boss Battle
Shooters are nice, but our game wouldn’t be a proper shoot ‘em up if it didn’t have a boss battle.
First let’s setup the sprite sheet pre-loading:
preload: function () {
this . load . spritesheet( ' whiteEnemy ' , 1 assets/shooting-enemy . png ' , 32, 32);
this . load . spritesheet( ' boss ' , 1 assets/boss . png ' , 93, 75);
this . load . spritesheet( ' explosion ' , 1 assets/explosion . png 1 , 32, 32);
this . load . spritesheet( ' player ' , ' assets/player . png ' , 64, 64);
},
Then the 'setupEnemies( ) " code:
setupEnemies : function () {
this. shooter Del ay = BasicGame. SPAWN J3H00TER_DELAY;
this . bossPool = this . add . group( ) ;
this . bossPool .enableBody = true;
this . bossPool . physicsBodyType = Phaser . Physics .ARCADE;
this . bossPool .createMultiple(l , 1 boss 1 ) ;
this . bossPool .setAll( ' anchor. x' , 0.5);
this . bossPool .setAll( ' anchor. y 1 , 0.5);
this . bossPool . setAll ( ' outOfBoundsKi 1 1 1 , true) ;
this . bossPool .setAll( ' checkWorldBounds ' , true);
this. bossPool .setAll( 'reward' , BasicGame . BOSS_REWARD, false, false, 0, true);
this . bossPool .setAll(
'dropRate', BasicGame.BOSS_DROP_RATE, false, false, 0, true
);
// Set the animation for each sprite
this . bossPool . forEach( function (enemy) {
enemy . animations . add( ' fly ' , [0, 1, 2 ], 20, true);
enemy . animations . add( ' hit ' , [ 3, 1, 3, 2 ], 20, false);
enemy .events. onAnimationComplete. add ( function (e) {
e. play( ' fly ' );
}, this);
});
this. boss = this . bossPool . getTop( ) ;
Afternoon 5: Expanding the Game
66
464
465
466
467
468
469
470
this . bossApproaching = false;
},
We made a group containing our single boss. This is for two reasons: to put the boss in the proper sprite
order - above the enemies, but below the bullets and text; and to step around the sprite vs sprite collision
coding quirk we mentioned way back. We also stored the actual boss in a property for convenience.
We then replace what happens when we reach 20,000 points from ending the game to spawning the boss:
addToScore function (score) {
this. score += score;
this . scoreText . text = this. score;
if (this. score >~ 20000) — f
this . c n c myPool . d c stroy( ) ;
this . shootorPool . dcstroy( ) ;
this . e n e myBul l e tPool . d e stroy( ) ;
this . displayEnd(truc) ;
f
// this approach prevents the boss from spawning again upon winning
if (this. score >= 20000 && this . bossPool . countDeadQ == 1) {
this . spawnBoss( ) ;
}
},
Then the new spawnBoss( ) function:
spawnBoss: function () {
this . bossApproaching = true;
this . boss . reset(this . game . width / 2, 0, BasicGame . BOSSJHEALTH) ;
this . physics . enable(this . boss, Phaser . Physics . ARCADE) ;
this . boss . body . velocity . y = BasicGame . B0SS_Y_VEL0C I TY;
this . boss ,play( ' fly' );
},
The bossApproaching flag is there to make the boss invulnerable until it reaches its target position. Let’s
add the code to processDelayedEf fects( ) to check this:
processDelayedEf fects : function () {
this . showReturn = false;
}
if (this . bossApproaching && this. boss. y > 80) {
this . bossApproaching = false;
this . boss . nextShotAt = 0;
this . boss . body . velocity . y = 0;
this. boss. body. velocity .x = BasicGame. B0SS_X_VEL0CITY;
Afternoon 5: Expanding the Game
67
// allow bouncing off world bounds
this. boss. body. bounce. x = 1;
this. boss. body. collideWorldBounds = true;
}
Once it reaches the target height, it becomes a 500 health enemy and starts bouncing from right to left
using the built-in physics engine.
Next is to setup the collision detection for the boss, taking into account the invulnerable phase:
checkCol 1 isions : function () {
this. player, this . powerUpPool , this . player Power Up, null, this
);
if (this . bossApproaching === false) {
this . physics . arcade . overlap (
this.bulletPool, this . bossPool , this . enemyHit, null, this
);
this . physics . arcade . overlap (
this. player, this . bossPool , this . playerHit, null, this
);
And modify the damageEnemy( ) to get our game winning condition back:
damageEnemy function (enemy, damage) {
enemy . damage(damage) ;
if (enemy . al ive) {
enemy . play ( 1 hit ' ) ;
} else {
this . explode(enemy ) ;
this . spawnPowerUp(enemy ) ;
this . addToScore( enemy . reward) ;
// We check the sprite key (e.g. ' greenEnemy ’ ) to see if the sprite is a boss
// For full games, it would be better to set flags on the sprites themselves
if (enemy. key === 'boss') {
this . enemyPool . destroy ( ) ;
this . shooterPool . destroy ( ) ;
this . bossPool . destroy ( ) ;
this . enemyBulletPool . destroy ( ) ;
this . di splay End (true) ;
}
}
We’ve saved the boss shooting code for last:
Afternoon 5: Expanding the Game
68
enemyFire functionQ {
}, this);
if (this . bossApproaching === false && this. boss. alive &&
this . boss . nextShotAt < this. time. now &&
this.enemyBulletPool .countDead() >= 10) {
this . boss . nextShotAt = this . time . now + BasicGame. BOSS_SHOT_DELAY;
for (var i = 0; i < 5; i++) {
// process 2 bullets at a time
var leftBullet = this . enemyBul letPool . getFirstExists( false) ;
leftBullet.reset(this.boss.x - 10 - i * 10, this. boss. y + 20);
var rightBullet = this . enemyBul letPool . getFirstExists( false) ;
rightBul let . reset(this . boss . x + 10 + i * 10, this. boss. y + 20);
if (this . boss . health > BasicGame. BOSS_HEALTH / 2) {
// aim directly at the player
this . physics . arcade . moveToObject(
leftBullet, this. player, BasicGame . ENEMY_BULLET_VELOCITY
);
this . physics . arcade . moveToObject(
rightBullet, this. player, BasicGame . ENEMY_BULLET_VELOCITY
);
} else {
// aim slightly off center of the player
this . physics . arcade . moveToXY (
leftBullet, this . player . x i * 100, this. player. y,
BasicGame. ENEMY_BULLET_VELOCITY
);
this . physics . arcade . moveToXY (
rightBullet, this . player . x + i * 100, this. player. y,
BasicGame. ENEMY_BULLET_VELOCITY
);
}
}
}
},
There are two additional phases to this boss fight after the “approaching” phase. First is where the boss
just fires 10 bullets concentrated to the player.
Afternoon 5: Expanding the Game
69
Then once the boss’s health goes down to 250, the boss now fires 10 bullets at the area around the player.
While this is the same amount of bullets as the previous phase, the spread makes it much harder to dodge.
Afternoon 5: Expanding the Game
70
Sound Effects
We’ve saved the sound effects for the end of the workshop because integrating it with the main tutorial
may make it more complicated that it should be.
Anyway, adding sound effects in Phaser is as easy as adding sprites. First, pre-load the sounds:
preload: function () {
this . load . spritesheet( ' player ' , ' assets/player . png ' , 64, 64);
this . load . audio( ' explosion ' , [ ' assets/explosion . ogg ' , ' assets/explosion . wav ' ] ) ;
this . load . audio ( ' playerExplosion ' ,
[ ' assets/player-explosion . ogg ' , ' assets/player-explosion . wav ' ] ) ;
this. load.audio( 'enemyFire' ,
[ ' assets/enemy- fire .ogg ' , 1 assets/enemy- fire. wav ' ] ) ;
this . load . audio ( ' playerFire ' ,
[ ' assets/player- fire . ogg ' , ' assets/player- fire . wav ' ] ) ;
this . load . audio( ' powerllp ' , [ ' assets/powerup . ogg ' , ' assets/powerup . wav ' ] ) ;
},
You can use multiple formats for each loaded sound; Phaser will choose the best format based on the
browser. Using Ogg Vorbis (.ogg) and AAC in MP4 (.m4a) should give you the best coverage among
browsers. WAV should be avoided due to its file size, and MP3 should be avoided for public projects due
to possible licensing issues.
Once loaded, we then initialize the audio, adding a new function setupAudio( ):
create: function () {
this . setupAudio( ) ;
this .cursors = this . input . keyboard . createCursorKeys( ) ;
},
setupAudio function () {
this .explosionSFX = this. add. audio( 'explosion' );
this . playerExplosionSFX = this . add . audio( ' playerExplosion ') ;
this .enemyFireSFX = this. add. audio( 'enemyFire' );
this . playerFireSFX = this . add . audio( ' playerFire ') ;
this . powerllpSFX = this . add . audio( ' powerllp ') ;
},
Then play the audio when they are needed. Enemy explosion:
Afternoon 5: Expanding the Game
71
damageEnemy function (enemy, damage) {
enemy . damage(damage) ;
if (enemy . al ive) {
enemy . play( 1 hit ' ) ;
} else {
this . explode(enemy ) ;
this . explosionSFX . play ( ) ;
this . spawnPowerllp(enemy ) ;
Player explosion:
playerHit: function (player, enemy) {
// check first if this . ghostUnti 1 is not not undefined or null
if (this . ghostUnti 1 && this . ghostUnti 1 > this . time . now) {
return;
}
this . playerExplosionSFX . play ( ) ;
// crashing into an enemy only deals 5 damage
Enemy firing:
enemyFire: functionQ {
enemy . nextShotAt = this . time . now + BasicGame . SHOOTER_SHOT_DELAY;
this . enemyFireSFX . play ( ) ;
}
}, this);
if (this . bossApproaching === false && this . boss . al ive &&
this . boss . nextShotAt < this . time . now &&
this . enemyBul letPool . countDead( ) >= 10) {
this . boss . nextShotAt = this . time . now + BasicGame . BOSS_SHOT_DELAY;
this . enemyFireSFX. play ( ) ;
for (var i = 0; i < 5; i++) {
Player firing:
Afternoon 5: Expanding the Game
72
fire: function() {
if (! this . player . al ive II this . nextShotAt > this . time . now) {
return;
}
this . nextShotAt = this . time . now + this . shotDelay ;
this . playerFireSFX . play ( ) ;
if (this . weaponLevel == 0) {
if (this . bul letPool . countDead( ) == 0) {
Power-up pickup:
playerPowerllp function (player, powerUp) {
this . addToScore( powerUp . reward) ;
powerUp . ki 1 1 ( ) ;
this . powerllpSFX . play ( ) ;
if (this . weaponLevel < 5) {
this . weaponLevel++;
}
},
Go ahead and play your game to check if the sounds are properly playing.
You might notice that the sound effects are pretty loud especially when you’re playing in a quiet room.
To wrap up this chapter, let’s adjust the game’s volume. It accepts a value between 0 and 1 so let’s pick
0.3:
setupAudio: function () {
this . sound . volume = 0.3;
this . explosionSFX = this . add . audio( 1 explosion 1 ) ;
this . playerExplosionSFX = this . add . audio( ' playerExplosion 1 ) ;
this . enemyFireSFX = this . add . audio( 1 enemyFire 1 ) ;
this . playerFireSFX this . add . audio( 1 playerFire ') ;
this . powerUpSFX = this . add . audio( ' powerUp ') ;
},
And now we’re done with the full game. We wrap up the tutorial in the next chapter.
Afternoon 6: Wrapping Up
We need to do one last thing before we unleash our game to the public.
Restore original game flow
At the start of the tutorial, we modified our game to skip directly to the Game state. Now that the game’s
done, we’ll need restore it to its original flow that we discussed in Afternoon 0.
Let’s start by deleting the preload( ) function in game . js:
BasicGame . Game . prototype = {
— pr e load : — function () — f
this . load . image( ' sea ' , — ' asscts/soa . png ' ) ;
this . load . imag e ( ' bul l o t ' , — ' ass o ts/bul l o t . png ' ) ;
this . load . image( ' cncmyBul lot ' , — ' assots/onomy bul lot . png ' ) ;
this . load . image( ' powcrupl ' , — ' assets/powcrupl . png ' ) ;
this . load . spr itcshoet( 1 grocnEncmy 1 , — ' assots/onomy . png 1 , — 33-; — 32 ) ;
this . load . spr itoshoot( 1 whitcEncmy 1 , — ' assots/shooting onomy . png ' , — 33-; — 32) ;
this . load . sprit c sh cc t( ' boss ' , — ' ass e ts/boss . png ' , 93, — 75) ;
this . load . spr itcshcct( ' explosion ' , — ' assots/oxplosion . png ' , — 33-; — 32) ;
this . load . sprit c sh cc t( ' play e r ' , — ' ass o ts/play o r . png ' , — &4r, — 6 4 ) ;
this . load . audio ( ' explosion ' , — [ ' assots/oxplosion . ogg ' , — 1 assots/oxplosion . wav 1 ] ) ;
this . load . audio( ' play e r Explosion ' ,
['assots/playor explosion . ogg ' , — 'assots/playor cxplosion.wav']);
this . load . audio( ' o n o myF ir o ' , —
['assots/onomy f iro. ogg', — 'assots/onomy firc.wav 1 ]);
this . load . audio ( ' p layer F iro ' ,
['assots/playor f iro. ogg', — 'assots/playor firc.wav']);
this . load . audio ( ' power Up ' , — [ ' assets/powerup . ogg ' , — ' assets/powerup . wav ' ] ) ;
create: function () {
Do the same for mainMenu . js:
BasicGame . MainMenu . prototype = {
— proload : — function () — f
this . load . imag e ( ' titl e pag e ' , — ' ass e ts/titl e pag e . png ' ) ;
hr
create: function () {
Revert the starting state in app . js to Boot:
Afternoon 6: Wrapping Up
74
// Now start the Boot state.
— game . state . start( ' Game ' ) ;
game . state . start( ' Boot ' ) ;
And before we forget, let’s destroy the sprites that we added in the previous chapter when we quit the
game:
quitGame function (pointer) {
// Here you should destroy anything you no longer need.
// Stop music, delete sprites, purge caches, free resources, all that good stuff.
this . sea . destroy( ) ;
this . player . destroy( ) ;
this . enemyPool . destroy( ) ;
this . but letPool . destroy( ) ;
this . explosionPool . destroy( ) ;
this . shooterPool . destroy ( ) ;
this . enemyBul letPool . destroy ( ) ;
this . powerllpPool . destroy ( ) ;
this . bossPool . destroy ( ) ;
this . instructions . destroy( ) ;
this . scoreText . destroy( ) ;
this . endText . destroy( ) ;
this . returnText . destroy( ) ;
// Then let's go back to the main menu.
this . state . start( ' MainMenu ' ) ;
}
Sharing your game
The good thing about HTML5 games is that it’s no different from a typical static HTML web site: if you
want to share your game to the world, all you need to do is find a web server, upload your files there, and
access the server through your browser.
If you used a cloud IDE like Codio or Nitrous. 10 for this tutorial, you don’t need to do anything - you can
just share the preview URL you used when you developed your game. If you developed locally, however,
you’ll need to decide from the thousands of web hosting solutions out there to host your game.
The only free hosting solution I can recommend right now is Github Pages. Most of the alternatives are
either seedy ad-infested sites or free-tier cloud solutions (e.g. AWS, Azure) that require a bit of tinkering
just to serve our game.
Unfortunately, Github Pages is not as easy as “drag-and-drop”; you still need to know Git before you can
use. So for the sake of those who aren’t experienced web developers, we’ll be using the simplest free static
web hosting out there that doesn’t bombard you with ads: Neocities.
Steps for deploying to Neocities
Neocities has a straightforward sign-up page and a simple drag-and-drop interface making it easy even
for beginners. There are some caveats, though:
Afternoon 6: Wrapping Up
75
• Neocities doesn’t support folders
• Neocities doesn’t allow you to upload . wav files
Taking these into account, here are the steps to using Neocities to host your game:
1. Remove all audio - Remove all of the load. audio/) calls in preloader, js and the add. audio/)
and audio, play ( ) calls in game. js. Refer to the previous chapter to find their locations.
2. Update asset locations - Move all images from the assets folder to the root then update all of the
references in boot . js and pre loader . js to point to the correct location i.e.
preload function () {
// Here we load the assets required for our preloader (in this case a loading bar)
this . load . image( 1 preloaderBar ' , ' preloader-bar . png ' ) ;
preload function () {
this . load . image( ' titlepage 1 , ' titlepage . png ' ) ;
this . load . image( 1 sea ' , ' sea . png ' ) ;
this . load . image( 'bullet', ’bullet. png 1 ) ;
this . load . image( 1 enemyBul let 1 , 1 enemy -bul let . png 1 ) ;
this . load . image( 1 powerupl 1 , 1 powerupl . png ' ) ;
this . load . spritesheet/ ' greenEnemy ' , 'enemy. png’, 32, 32);
this . load . spritesheet/ ' whiteEnemy ' , ' shooting-enemy . png ' , 32, 32);
this . load . spritesheet/ ’ boss ' , ' boss . png ' , 93, 75);
this . load . spritesheet/ ' explosion ' , ’ explosion . png ' , 32, 32);
this . load . spritesheet/ ' player ' , ' player . png ' , 64, 64);
}
3. Sign-up for Neocities - Fill up the form at https://neocities.org/new.
4. Overwrite index.html - Replace its contents with your index.html.
<- C | s https://neodties.org/site_filesAext_edtor/index.html ® =
Afternoon 6: Wrapping Up
76
5. Upload the game files - Drag and drop all of the . js and . png files as well favicon . ico to the files
box. If dragging multiple files doesn’t work, upload them one by one.
<- -> C | 2 https://neodties.org/dashboard
**1 =
Websites Tutorials API About Support Us
Dashboard Settings Signout
j
My Website
Last updated right now. this very moment.
Using 2.7% (0.54MB) of your 20 MB. Need more space?
0 hits
Allowed file types | Download entire site
6. Verify the game works by opening the site - If all goes well, you should now see your game (sans
sound).
Evening: What Next?
Congratulations! You’ve just created and deployed your first HTML5 game!
Your journey is far from over, though, and in this chapter we’ll go through your next steps.
Challenges
A common problem with coding workshops is that some participants think they have already grasped the
concepts well when in reality they just knew how to correctly copy-paste the code examples. Prove that
you’re not one of those people by taking on the following challenges:
• Add bombs to the game
0
Use Arrow Keys to Move, Press Z to Fire
Tapping/clicking does both
Press X or click the top left icons to trigger Bomb
'I'if*
Players start with 3 bombs, the current count represented by icons on the top left corner of the
scene. Pressing X or tapping one of these icons will trigger the bomb, continuously destroying all
enemy bullets and dealing a small amount of damage for a few seconds. This is usually done the
moment before an enemy bullet collides with the player.
Evening: What Next?
78
Use Arrow Keys to Move, Press Z to Fire
Tapping/clicking does both
■ click the top left icons to trigger 1
Hint: Use the bomb, png as the icon and bomb-blast . png as the effect that will cover the whole
screen colliding with all enemies.
• Use the second power-up
There’s an additional power-up image in the assets folder. Use it to give the player a speed boost
or a different weapon. For example, here we made the red power-up give a concentrated shot which
can be more effective in the boss battle:
1800
■rlfl-
• Create a difficulty progression
Apart from the boss fight at 20000 points, the difficulty stays the same for most of the game session.
Add some flags and additional checking to make the game slightly more difficult as the game
progresses. For example:
Score
Enemy Spawn Rate
Shooter Spawn Rate
Boss
0 - 2000
1.0s
n/a
n/a
2000 - 5000
0.8s
3.0s
n/a
5000 - 10000
0.6s
2.5s
n/a
10000 - 17500
0.5s
2.0s
n/a
17500 - 25000
0.3s
1.5s
n/a
> 25000
0.6s
stop spawning
spawn the boss
Evening: What Next?
79
• Add new patterns and phases to the boss fight
The patterns can be movement patterns (not just bouncing left and right) and shooting patterns.
A classic shoot ‘em up pattern
• Display the breakdown of kills at the end of the game
52200
You Win!!!
Your kills:
x 228
nlr. , 43
x 1
Press Z or Tap Game to go back to Main Menu
VljlT
• Add new enemies: the destroyer and the sub
There are two unused enemy sprite sheets: one for a destroyer and one for a submarine. Being sea
units, they will have to behave a bit differently from their flying counterparts, namely, they are
Evening: What Next?
80
below all other flying sprites, and they can’t overlap with each other.
• Refactor parts of the code.
There are still places where the code is duplicated 3 or more times. Turn them into functions to
reduce the code size.
You can also try converting some of the game objects into JS objects. The Tank example in Phaser
Examples is way to implement this.
• Convert time-related events to use Phaser’s time classes
Many of the time-related code in our game only uses the current time as reference. This results in
incorrect behavior in certain situations (e.g. pausing the game).
Replace those code with the appropriate Time and Timer functions. See the Time section of Phaser
Examples for ideas on how to do this.
What we didn't cover
We’ve skipped a lot of Phaser topics. Here are some topics you might want to look into after this workshop:
• Other Phaser settings (e.g. auto-scale, pause on lose focus)
• Background music
• Mobile support (e.g. additional features, packaging to app stores)
• P2 physics
• Performance tuning and Debugging
• Persisting data
• Interacting with libraries and APIs
Many of these are covered by the official documentation and by some of the tutorials on this list. For the
rest, feel free to ask about them at the official Phaser forum.
We also did not cover how to prepare assets for your game. There are lists of free resources out there like
this wiki page (which also lists where we got our sounds, OpenGameArt.org). You can also Google for
assets, but you have to check their licenses and see if you can use them in your games.
Processing assets is also something that is out of the scope of this tutorial. For example, our art assets
came from SpriteLib but they had to be converted into sprite sheets that are compatible with Phaser (e.g.
convert blue to transparent, add damage effect to enemy, etc.), and the volume of our sound assets had to
be tweaked a bit.
For image editing, you can look for Paint.NET and Gimp tutorials. For sound editing, you check out
Audacity tutorials.
Appendix A: Environment Setup Tutorials
This section is divided into 3 sections. The Basic section which provides the most basic ways of setting up
your development environment for Phaser, the Advanced section which are for experienced developers
who want a more comfortable environment at the price of complexity, and the Cloud section where we
have tutorials on how to develop without requiring anything other than a browser and a stable internet
connection.
We’re using an unstable Phaser release (2.4-rcl) in this book. Ideally, we should be using the
stable 2.3 build but unfortunately the official build does not have sprite health included.
If you’ve finished this book and you’re encountering problems as you’re poking around with
the Phaser library included in the template, you can try downgrading to version 2.2.2 or wait
until the stable 2.4 is released.
Basic Setup
Here’s a basic step-by-step tutorial on preparing your system for the workshop:
1. Download the basic game template from Github and extract it into a folder.
2. Download either version 2 or beta version 3 of Sublime Text and install it in your computer.
3. In Sublime Text, add the folder you extracted to the current project by using Project -> Add Folder
to Project . . .
Appendix A: Environment Setup Tutorials
82
4. This last step, setting up a web server, will depend on your operating system:
If you’re using Windows, the smallest and easiest web server to setup is Mongoose.
If you’re using a Mac or a Linux / Unix machine, the easiest is Python’s SimpleHTTPServer since
pretty much all of these OSs have Python pre-installed.
Mongoose Setup
Repeating Mongoose’s tutorial:
1. Download Mongoose Free Edition and copy it into the working folder.
f CCesanfc, Software 7 \ _ I = I M I SS I
<- C | D cesanta.com/mongoose.shtm l jftj =
downloads since 2004 plus: . Ability to view and analyze all
Cross-platform: works on Mac. • CGI (ability to run sites written in incoming and outgoing network
Windows. UNIX/Linux PHP. Ruby or any other scnpting packets
© Cesanta Software Limited V. +353 1 25 44 770
& support@cesanta i
o LCJLU
s <• ssz
2. Run Mongoose. Unblock the firewall for Mongoose by clicking A1 low access.
Appendix A: Environment Setup Tutorials
83
3. Your browser should now be open at the game template.
Starting a Simple Python HTTP Server
1. Open your terminal and go to your working folder.
2. Run python -m SimpleHTTPServer.
3. Open your browser to http://localhost:8000/ to access your game.
Appendix A: Environment Setup Tutorials
84
1 Downloads/html5shmup-template-m
luser@test - Downloads/htmlSshmup- template-master $ python -m SimpleHTTPServ
Serving HTTP on e. 0.0.0 port 8000 ...
127.0.0.1 - - [01/Jul/2014 00:10:22] "GET / HTTP/1.1" 200 -
- [01/Jul/2014 00:10:22] "GET /phaser-arcade-physics.min.js HTTP/1.1" 200l
Y.AJ
1 12
(YET ANOTHER W(“
Press Z or ta|
image assets Cop;
sound assets Copyrigl
- [01/Jul/2014 0
- [01/Jul/2014 0
- [01/jul/2014 0
- [01/Jul/2014 0
- [01/Jul/2014 0
- [01/Jul/2014 0
- (01/Jul/2014 0
- [Ol/Jul/2014 0
- [01/Jul/2014 0
- [01/JUI/2014 0
- [01/Jul/2014 0
• (01/JUI/2014 0
- [01/Jul/2014 0
- [Ol/Jul/2014 0
- (Ol/Jul/2014 0
- [Ol/Jul/2014 0
- [Ol/Jul/2014 0
- [Ol/Jul/2014 0
- [Ol/Jul/2014 0
- [Ol/Jul/2014 0
- [Ol/Jul/2014 0
• [Ol/Jul/2014 0
10:23]
10:23]
10:23]
■GET /boot . j S HTTP/1.1" 200 -
•GET /preloader. js HTTP/1.1" 200 -
•GET /mainMenu.js HTTP/1.1" 200 -
•GET /game . j s HTTP/1.1" 200 -
•GET /app.js HTTP/1.1" 200 -
■GET /assets/preloader-bar. png HTTP/1.1" 200 -
•GET /favicon. ico HTTP/1.1" 200 -
■GET /assets/titlepage . png HTTP/l.l" 200 -
■GET /assets/sea. png HTTP/l.l" 200 -
•GET /assets/bullet. png HTTP/l.l" 200 -
•GET /assets/enemy -bullet. png HTTP/l.l" 200 -
■GET /assets/powerupl . png HTTP/l.l" 200 -
•GET /assets/enemy. png HTTP/l.l" 200 -
•GET /assets/shooting-enemy. png HTTP/l.l" 200 -
■GET /assets/boss. png HTTP/l.l" 200 -
•GET /assets/explosion. png HTTP/l.l" 200 -
"GET /assets/player. png HTTP/l.l" 200 -
■GET /assets/explosion. wav HTTP/l.l" 200 -
•GET /assets/player-explosion. wav HTTP/l.l" 200 ||
•GET /assets/enemy-fire. wav HTTP/l.l" 200 -
■GET /assets/player-fire. wav HTTP/l.l" 200 -
•GET /assets/powerup.wav HTTP/l.l" 200 -
' ^ ■thtmlSsh,
Advanced Setup
Some experienced web developers might open the basic template and be disappointed at how plain it
looks compared to the code they use in their day to day work. To answer this problem, I’ve made a couple
of alternative templates that have the 2 features that I can’t live without when developing front-ends:
LiveReload and a means for concatenating/minifying/preprocessing JS and CSS.
JavaScript / NodeJS Template
You can find a starting template for NodeJS at the javascript branch of the base template.
This template is a slightly modified version of Luke Wilde’s phaser-js-boilerplate which uses Browserify,
Jade, Stylus, Lodash, JsHint, Uglify.js, Google Analytics, Image optimisation tools, LiveReload, Zip
compression, and partial cache busting for assets.
To setup:
$ git clone https://github.com/bryanbibat/html5shmup-template.git
$ cd html5shmup-template
$ git checkout javascript
$ npm install
Run grunt (which you might have to install via npm install -g grunt -c 1 i) to start server and open the
default browser to http://localhost:3017. You can change the port settings in src/ js/game/properties . js.
Run grunt build to compile everything (pre-process, concatenate, minify, etc.) to the build folder for
production release.
Refer to the original boilerplate’s Github Read Me for other details.
Appendix A: Environment Setup Tutorials
85
Ruby Template
You can find a starting template for Ruby at the ruby branch of the base template.
This template uses Middleman for features like LiveReload and Asset Pipeline. Compared to the NodeJS
template, this template’s set of libraries are more oriented towards the Ruby ecosystem: ERb and Haml
instead of Jade, Sass instead of Stylus, and so on.
To setup:
$ git clone https://github.com/bryanbibat/html5shmup-template.git
$ cd html5shmup-template
$ git checkout ruby
$ bundle install
To start server:
$ bundle exec middleman server
Your game will be available at http://localhost:4567. Note that LiveReload is set up to work only for
localhost, if you want to make it work on a different machine in the network, you must specify the host
in conf ig . rb e.g.
activate :livereload, host: 192.168.1.111
To compile everything to the build folder:
$ bundle exec middleman build
Refer to the Middleman docs for other details.
Note that Middleman’s Sprockets interface doesn’t support audio so audio_path won’t work. Check out
_pre loader . js . erb for my workaround.
Cloud IDE Setup
Online IDEs like Codio and Cloud9 serve as alternative to desktop/laptop-based development. They take
away the hassle of having to install additional software on your computer and replace it with the hassle
of finding a venue that has reliable internet - this can be a big problem for workshops.
We’ll run through the steps of setting up two types development environment: one using Codio on the
basic template, and another using Nitrous. 10 on the advanced Ruby template.
Appendix A: Environment Setup Tutorials
86
Codio + Basic Template
1. Sign-up for Codio by filling out the form at https://codio.eom/p/signup.
2. At the dashboard, click “New Project”. Click the more options link (“Click here”), choose Import
and enter the Git URL https://github.com/bryanbibat/html5shmup-template.git. Fill out the project
name and description then click the “Create” button.
3. Wait until Codio finishes creating your project. You should be able to start editing your files once
that is done.
■'Ci
4. Click the “Project Index (static)” button/link at the header to open your game in a new tab. You can
also access your game by opening the URL shown in another browser tab or window.
Appendix A: Environment Setup Tutorials
87
Cloud9 + NodeJS Template
1. Sign-up for Cloud9 by filling out the form at https://c9.io/web/sign-up/free.
2. At the dashboard, click “Create a new workspace”. Fill out the details and the Git URL, choose the
“Node.js” template, and click the “Create Box” button. Leave the Github repository blank.
- sifaisi-g-i-
*- -* C I i https://c9.io/new ■£? 5
Create a new workspace
Workspace name
htmISshmup
Description
Making an HTML5 shmup on the cloud
Already have your own development machine in the cloud? Create a new cloud9 workspace that connects to it rioht here.
Hosted workspace
n Private , 4 . Public
This is a workspace for your eyes on(y This wilt create a workspace for everybody to see
Clone from Git or Mercurial URL (optional)
https://github.com/bryanbibat/html5shmup-template.git
Choose a template
®
HTML
0
n*de»
%
Meteor PHP. Apache 8 MySQL
a:
A
3. Wait until Cloud9 finishes creating your project. Once that is done, go to the bash console at the
bottom and checkout the javascript branch then install the required modules:
$ git checkout javascript
$ npm install -g grunt-cli
$ npm install
You can also choose your preferences at the Welcome screen while the installation is in progress.
Appendix A: Environment Setup Tutorials
88
<- C | S https://ide.c9.io/bry_bibat/html5shmup
*1 =
Cloud9 File Edit Find View Goto Run Tools Window Support Preview O Run
s V li htmISstvnup * Welcome
iT 3,316
g ► fe node_modules
•EL fSk Welcome
Cloud9 IDE -Your Code Anywhere, ...
B README-md
Welcome to CloudSt. Use this welcome screen to tweak the look a ted otthe Cloud9 user interlace
Choose a Preset
° H
V FuBlOE Mnmal Editor SuMmeMode
Cloud9 - The introduction
the Cloud9 Blog
Since the dark ages when the green on
black screens were the only Interface to a
machine, me terminal has been a coders
besltlend
bash -Cloning * Immediate x +
i!5
| — buffer02.8.2 (ieee 75401. 1.6, base64- js00.0.7, is-array01.8.1)
| — J SONS treaa00. 8.4 ( jsonparse00.0.5, through02.3.8)
| — browser- resolvent. 9.0 (resolve01.1.6)
| — deps- sort00. 1.2 (through02. 3.8, winiwist00.0.10, 3SONStreaw00.6.4)
( — syntax-error01.1.4 (acorn01.2.2)
| — browser-pack02.0.1 (through02.3.B, cowbine-source-wap00.3.0, lSONStrea^B.6.4)
browserify- zlib00. 1 .4 (pako00.2.7)
| — insert -wodule-globals06. 0.0 (process00.6.0, through02.3.8, JSflNStreaw00.7.4, le»ical-scope01.1.1)
| — uwd02.1.0 (through02.3.8, rfilefl.0.0, uglify- JS02.4.23, ruglifypi.0.0)
| — wodule-deps02.1.5 (parents00.0.2, duplexer200.0.2, ainiaist00.0. 10, streaw- cowbiner00. 1.0, resolve00.6
tive03.1.0)
| — derequire0O.8.0 (estraverse01.5.1, esrefactor00.1.0, espri«a-fb03801.1.0-dev-harwony-fb)
■— crypto-browse rify02. 1.10 (ripewdl6008.2.0, sha. js02.1.6)
grunt-contrib-connect00.7.1 node_aodules/grunt-contrib-connect
|— connect- livereload00. 3.2
|— open00.0.4
| — async00.2.10
|— portscanne 1-08.2. 2 (async00.1.15)
1 — connect^. 13.1 <uid200.0.3, wethods00.1.0, debug08.8.1, cookie-signature01.0.1, pause00.0.1, fresh00.2
ch00.5.0, cookie0O.1.0, cowpressible01.0.0, negotiator^. 3.0, sendee. 1.4, «jltiparty02.2.0)
bry_bibatR*Ttml5sh*mjp: -/workspace (javascript) S |
3, thro ugh 200. 4. 2, browser- re sal ve01 .2.4, 3SONStrean00.7.4, detec
0, qs00.6.6, buffer-crc 3208. 2.1, bytes0O.2.1, raw-body01.1.3, bat
4. To view our app, Cloud9 directs traffic to one port and IP address defined by the PORT and I P env
variables respectively. Open gruntf i le . js, modify the connect options accordingly:
connect. {
dev : {
options: {
port : — ' <% = proj e ct. port %> ' ,
port: process. env. PORT,
hostname process . env . IP,
base : ' . /build 1
}
}
},
Run grunt - - force to start the app and ignore the grunt-open error:
5. Open “Preview -> Preview Running Application” to open your game in a new window.
Appendix A: Environment Setup Tutorials
89
You can also press the “Pop Out Into New Window” button to open the preview in a new tab.
You can now edit your files and make your game. Unfortunately since Cloud9 only opens one port,
LiveReload will not refresh the game automatically upon saving.
Appendix B: Expected Code Per Chapter
We understand that there are cases you need to “cheat” and need to look for the “correct” code after each
chapter.
Maybe you’ve been spending too much time trying to find where you mistyped the code. Maybe a
participant had to leave the workshop for 1 hour to deal with an emergency.
Regardless of the reason, here are the working code for each chapter of the workshop.
• Overview of the Starting Code - Browse in Github, Download Zip
• Sprites, the Game Loop, and Basic Physics - Browse in Github, Download Zip
• Player Actions - Browse in Github, Download Zip
• Object Groups - Browse in Github, Download Zip
• Refactoring - Browse in Github, Download Zip
• Health, Score, and Win/Lose Conditions - Browse in Github, Download Zip
• Expanding the Game
- Harder Enemy - Browse in Github, Download Zip
- Power-up - Browse in Github, Download Zip
- Boss Battle - Browse in Github, Download Zip
- Sound Effects - Browse in Github, Download Zip
• Wrapping Up
- Restore original game flow - Browse in Github, Download Zip
- Sharing your game - Browse in Github, Download Zip
To make sure the lazy people don’t cheat all the way, we won’t provide links to solutions for the Challenges.